<?php

namespace App\Libraries;

use App\Models\BookingModel;
use App\Models\ConciergeConversationModel;
use App\Models\ConciergeMessageModel;
use App\Models\EventModel;
use App\Models\FolioModel;
use App\Models\GuestModel;
use App\Models\MenuItemModel;
use App\Models\MessageLogModel;
use Throwable;

/**
 * WhatsApp Concierge — the last Phase 5 AI feature (Tier 2, guest-facing).
 * ConciergeWebhookController calls handleInboundMessage() for every real
 * inbound WhatsApp message; this class owns all the routing/grounding/
 * guardrail logic so the webhook controller stays a thin HTTP adapter
 * (same split as DailyBriefingService vs. the ai:daily-briefing command).
 *
 * Grounding rule (see the Phase 5 AI brief): a message is only ever
 * routed to Claude if it comes from a real, phone-matched guest who has
 * an active (checked-in) or upcoming (confirmed, not yet arrived)
 * booking — see BookingModel::activeOrUpcomingForGuest(). Anything else
 * (unrecognized number, guest with no qualifying booking, or the feature
 * turned off) is captured and flagged for staff, never auto-answered,
 * since there's no safe context to ground a reply in.
 *
 * Scope boundary: Claude only ever has read-only tools (guest's own
 * booking/folio status, hotel info, menu highlights, upcoming events)
 * plus one escalate_to_staff tool it must call instead of attempting to
 * handle a booking-change/rate/payment/complaint intent — see the system
 * prompt in buildSystemPrompt(). It can never modify a booking, quote a
 * new rate, or process a payment.
 *
 * Sending: a drafted reply only goes out immediately if the tenant has
 * opted this category into autosend (ai.autosend_concierge_replies,
 * default off, per the brief's guardrails) — otherwise it's stored as a
 * pending outbound message and the conversation is marked pending_reply
 * for a staff member to review and send from the Concierge inbox
 * (ConciergeController).
 */
class ConciergeService
{
    public function __construct(
        private readonly ClaudeService $claude,
        private readonly WhatsAppService $whatsApp,
    ) {
    }

    public function handleInboundMessage(string $fromPhone, string $body, ?string $whatsappMessageId): void
    {
        $normalizedPhone = PhoneNumber::normalize($fromPhone) ?? $fromPhone;

        $messages = new ConciergeMessageModel();

        if ($whatsappMessageId !== null && $messages->alreadyReceived($whatsappMessageId)) {
            log_message('info', "ConciergeService: duplicate inbound message {$whatsappMessageId}, ignoring.");

            return;
        }

        $conversations = new ConciergeConversationModel();
        $conversation  = $conversations->findOpenForPhone($normalizedPhone);

        if ($conversation === null) {
            $conversationId = $conversations->insert(['phone' => $normalizedPhone]);
            $conversation   = $conversations->find($conversationId);
        }

        $messages->insert([
            'conversation_id'      => $conversation->id,
            'direction'            => 'inbound',
            'status'               => 'sent',
            'body'                 => $body,
            'whatsapp_message_id'  => $whatsappMessageId,
        ]);

        $conversations->update($conversation->id, ['last_message_at' => date('Y-m-d H:i:s')]);

        if (service('settings')->get('ai.concierge_enabled', '0') !== '1') {
            $conversations->update($conversation->id, ['needs_staff' => 1]);

            return;
        }

        $guest = (new GuestModel())->where('phone', $normalizedPhone)->first();

        if ($guest !== null && $conversation->guest_id === null) {
            $conversations->update($conversation->id, ['guest_id' => $guest->id]);
        }

        $booking = $guest !== null ? (new BookingModel())->activeOrUpcomingForGuest($guest->id) : null;

        if ($guest === null || $booking === null) {
            // Not enough safe context to ground an AI reply — a human
            // needs to look at this (unrecognized number, or a real
            // guest with no active/upcoming stay to answer questions
            // against).
            $conversations->update($conversation->id, ['needs_staff' => 1]);

            return;
        }

        $this->respond($conversation, $guest, $booking, $messages);
    }

    private function respond(object $conversation, object $guest, object $booking, ConciergeMessageModel $messages): void
    {
        $escalated = false;

        $tools = [
            [
                'name'        => 'get_guest_status',
                'description' => "This guest's own real booking and folio status — never another guest's.",
                'input_schema' => ['type' => 'object', 'properties' => new \stdClass()],
            ],
            [
                'name'        => 'get_hotel_info',
                'description' => 'Real check-in/check-out times, wifi info, and general policy/directions text the hotel has configured.',
                'input_schema' => ['type' => 'object', 'properties' => new \stdClass()],
            ],
            [
                'name'        => 'get_menu_highlights',
                'description' => 'A sample of the real restaurant menu, grouped by category with real prices.',
                'input_schema' => ['type' => 'object', 'properties' => new \stdClass()],
            ],
            [
                'name'        => 'get_upcoming_events',
                'description' => 'Real upcoming confirmed hotel events (name, date, venue).',
                'input_schema' => ['type' => 'object', 'properties' => new \stdClass()],
            ],
            [
                'name'        => 'escalate_to_staff',
                'description' => 'Call this instead of attempting to answer yourself whenever the guest is asking about a booking change, a rate/price for a new stay or change, a payment, or a complaint — or is clearly frustrated. Do not attempt to resolve these yourself.',
                'input_schema' => [
                    'type'       => 'object',
                    'properties' => ['reason' => ['type' => 'string', 'description' => 'Short internal note for staff on why this was escalated.']],
                    'required'   => ['reason'],
                ],
            ],
        ];

        $executor = function (string $name, array $input) use ($guest, $booking, &$escalated) {
            if ($name === 'get_guest_status') {
                return json_encode($this->guestStatusTool($guest, $booking));
            }

            if ($name === 'get_hotel_info') {
                return json_encode($this->hotelInfoTool());
            }

            if ($name === 'get_menu_highlights') {
                return json_encode($this->menuHighlightsTool());
            }

            if ($name === 'get_upcoming_events') {
                return json_encode($this->upcomingEventsTool());
            }

            if ($name === 'escalate_to_staff') {
                $escalated = true;
                log_message('info', "ConciergeService: escalated — " . ($input['reason'] ?? 'no reason given'));

                return json_encode(['acknowledged' => true]);
            }

            return json_encode(['error' => 'Unknown tool.']);
        };

        $result = $this->claude->chat(
            'concierge',
            $this->buildSystemPrompt($guest),
            $messages->recentAsMessages($conversation->id),
            $tools,
            $executor,
            'concierge_conversation',
            $conversation->id,
            null,
        );

        $conversations = new ConciergeConversationModel();

        if (! $result['success']) {
            $conversations->update($conversation->id, ['needs_staff' => 1]);

            return;
        }

        $replyText = trim((string) $result['text'], " \t\n\r\0\x0B\"");
        $autosend  = service('settings')->get('ai.autosend_concierge_replies', '0') === '1';

        $update = ['needs_staff' => $escalated ? 1 : 0];

        if ($autosend) {
            $sendOutcome = false;
            $raw         = '';

            try {
                $outcome     = $this->whatsApp->sendFreeformText($conversation->phone, $replyText);
                $sendOutcome = $outcome['success'];
                $raw         = $outcome['raw'];
            } catch (Throwable $e) {
                $raw = $e->getMessage();
            }

            $messages->insert([
                'conversation_id' => $conversation->id,
                'direction'       => 'outbound',
                'status'          => $sendOutcome ? 'sent' : 'failed',
                'body'            => $replyText,
            ]);

            (new MessageLogModel())->record($guest->id, 'whatsapp', 'ai_concierge', $conversation->phone, $sendOutcome, $raw, $replyText, null);

            $update['pending_reply'] = 0;

            // A failed autosend leaves the guest with no reply at all —
            // that's not a "handled" conversation just because it wasn't
            // an escalation, so it must still surface in the Needs Staff
            // queue rather than silently vanish from the inbox.
            if (! $sendOutcome) {
                $update['needs_staff'] = 1;
            }
        } else {
            $messages->insert([
                'conversation_id' => $conversation->id,
                'direction'       => 'outbound',
                'status'          => 'pending',
                'body'            => $replyText,
            ]);

            $update['pending_reply'] = 1;
        }

        $conversations->update($conversation->id, $update);
    }

    /**
     * The one real send path for a staff-approved pending reply — called
     * from ConciergeController::sendReply(). Mirrors GuestMessageDraftController::
     * reallySend()'s shape exactly, just over WhatsApp instead of SMS,
     * since a concierge reply is answering the guest on the same channel
     * they messaged us on.
     */
    public function sendPendingReply(object $conversation, object $message, ?int $guestId): bool
    {
        $success = false;
        $raw     = '';

        try {
            $outcome = $this->whatsApp->sendFreeformText($conversation->phone, $message->body);
            $success = $outcome['success'];
            $raw     = $outcome['raw'];
        } catch (Throwable $e) {
            $raw = $e->getMessage();
        }

        (new ConciergeMessageModel())->update($message->id, ['status' => $success ? 'sent' : 'failed']);
        (new ConciergeConversationModel())->update($conversation->id, ['pending_reply' => 0]);
        (new MessageLogModel())->record($guestId, 'whatsapp', 'ai_concierge', $conversation->phone, $success, $raw, $message->body, null);

        return $success;
    }

    private function guestStatusTool(object $guest, object $booking): array
    {
        $folio   = (new FolioModel())->findOpenForGuest($guest->id);
        $balance = $folio !== null ? (new FolioModel())->balanceDue($folio->id) : null;

        return [
            'guest_name'        => $guest->full_name,
            'booking_status'    => $booking->status,
            'room_number'       => $booking->room_number,
            'room_type'         => $booking->room_type_name,
            'check_in_date'     => $booking->check_in_date,
            'check_out_date'    => $booking->check_out_date,
            'open_folio_balance_due' => $balance,
        ];
    }

    private function hotelInfoTool(): array
    {
        $settings = service('settings');

        return [
            'check_in_time'   => $settings->get('hotel.check_in_time') ?: null,
            'check_out_time'  => $settings->get('hotel.check_out_time') ?: null,
            'wifi_info'       => $settings->get('hotel.wifi_info') ?: null,
            'general_info'    => $settings->get('hotel.guest_info_faq') ?: null,
            'note'            => 'A null field means the hotel has not configured that information yet — do not guess it, tell the guest you will check with the front desk.',
        ];
    }

    private function menuHighlightsTool(): array
    {
        $grouped   = (new MenuItemModel())->groupedByCategory();
        $highlights = [];

        foreach ($grouped as $category => $items) {
            $highlights[$category] = array_map(
                static fn ($item) => ['name' => $item->name, 'price' => $item->price],
                array_slice($items, 0, 4),
            );
        }

        return $highlights;
    }

    private function upcomingEventsTool(): array
    {
        $events = (new EventModel())
            ->where('event_date >=', date('Y-m-d'))
            ->where('status', 'confirmed')
            ->orderBy('event_date', 'asc')
            ->limit(5)
            ->findAll();

        return array_map(
            static fn ($e) => ['name' => $e->event_name, 'date' => $e->event_date, 'venue' => $e->venue_space],
            $events,
        );
    }

    private function buildSystemPrompt(object $guest): string
    {
        $hotelName = service('tenantContext')->getTenant()->hotel_name;
        $voice     = service('settings')->get('ai.brand_voice') ?: 'professional, warm, and welcoming';

        return "You are the WhatsApp concierge for {$hotelName}, texting with {$guest->full_name}, a real guest "
            . 'whose active or upcoming booking has already been verified before you were called — you do not need '
            . "to re-verify this. Match this tone: {$voice}. Keep replies short and natural, like a real WhatsApp "
            . "text (1-4 sentences), never a long formal message.\n\n"
            . "Use get_guest_status for this guest's own real booking/folio status, get_hotel_info for check-in/"
            . 'check-out times/wifi/general policies, get_menu_highlights for restaurant questions, and '
            . "get_upcoming_events for event questions. Never state a fact these tools didn't give you — if a tool "
            . "field is empty, say you'll check with the front desk rather than guessing.\n\n"
            . 'You must NEVER: quote a rate/price for a new booking or a change to this one, confirm/modify/cancel a '
            . 'booking, process a payment, or promise a discount/refund/compensation. For ANY of those, or if the '
            . 'guest is clearly frustrated or complaining, call escalate_to_staff with a short reason and reply with '
            . 'a brief, warm message saying a team member will follow up shortly — do not attempt to resolve it '
            . 'yourself.';
    }
}
