<?php

namespace Modules\WhatsApp\Entities\Services;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Modules\WhatsApp\Entities\WaContact;
use Modules\WhatsApp\Entities\WaConversation;
use Modules\WhatsApp\Entities\WaMessage;
use Modules\WhatsApp\Entities\WaSetting;

/**
 * Inbound AI auto-reply. Grounds the model in real ERP data (live product stock
 * + the customer's appointments) so it answers questions like
 * "do you have flea meds for cats?" or "when is my dog's next visit?" — and
 * hands off to a human when it can't help.
 *
 * Uses the shared AiCore engine (AiProvider settings + AiClient + ToolRunner +
 * the business-type-aware PromptBuilder) when present. Soft dependency: if
 * AiCore is not installed the AI path is silently skipped — never fatals.
 * Never throws into the caller.
 */
class WaAiResponder
{
    public function respond(WaMessage $inbound, WaConversation $conversation): void
    {
        try {
            $businessId = (int) $inbound->business_id;

            // Master + per-thread switches, and never speak over a human agent.
            if (WaSetting::get($businessId, 'ai_enabled', '0') !== '1') {
                return;
            }
            if (!$conversation->ai_enabled || $conversation->assigned_user_id) {
                return;
            }

            $isImage = $inbound->type === 'image' && !empty(($inbound->payload ?? [])['media_path']);
            $text = trim((string) $inbound->body);
            if ($text === '' && !$isImage) {
                return;
            }

            // Opt-out keyword.
            $optOut = strtoupper(trim((string) WaSetting::get($businessId, 'opt_out_keyword', 'STOP')));
            if ($optOut !== '' && strtoupper($text) === $optOut) {
                WaContact::where('business_id', $businessId)->where('wa_id', $inbound->wa_id)
                    ->update(['opted_out' => true]);
                $this->reply($conversation, "You've been unsubscribed and won't receive further messages. Reply START to opt back in.");
                return;
            }
            if (strtoupper($text) === 'START') {
                WaContact::where('business_id', $businessId)->where('wa_id', $inbound->wa_id)
                    ->update(['opted_out' => false]);
            }

            // Need the shared AiCore engine. Soft dependency — if it's not
            // installed, leave the conversation for staff (no fatal).
            if (!class_exists(\Modules\AiCore\Entities\AiProvider::class)
                || !class_exists(\Modules\AiCore\Entities\Services\ToolRunner::class)) {
                return;
            }
            $settings = \Modules\AiCore\Entities\AiProvider::forBusiness($businessId);
            if (!$settings->isConfigured()) {
                return;
            }

            $business = \App\Business::find($businessId);
            $businessName = $business->name ?? 'our business';

            // If the customer sent a photo, read it (vision OCR). Keep the raw
            // identifiers separate so we SEARCH on them (not the wrapper text).
            $effectiveText = $text;
            $searchText = $text;
            if ($isImage) {
                $extracted = (new WaVisionExtractor())->extractFromMessage($settings, $inbound);
                if ($extracted !== '') {
                    $effectiveText = trim($text . "\n[Customer sent a photo of a product. Identifiers read from the image: {$extracted}]");
                    $searchText = $extracted;   // ground the product search on the real identifiers
                } else {
                    $effectiveText = trim($text . "\n[Customer sent a photo, but no clear identifiers could be read from it.]");
                }
            }

            // Identify the customer: greet a registered contact by name, or ask a
            // new guest for their name first (deterministic). May reply itself and
            // stop the AI for this turn.
            $journey = (new WaCustomerJourney())->handle($inbound, $conversation, $businessName, $effectiveText);
            if (!$journey['proceed']) {
                return;
            }
            $question = $journey['question'] !== '' ? $journey['question'] : $effectiveText;
            // When answering a remembered (pending) question, search on it too.
            if (!$isImage) {
                $searchText = $question;
            }

            // Appointment booking is an opt-in capability (off by default).
            $canBook = WaSetting::get($businessId, 'ai_can_book', '0') === '1'
                && class_exists(\Modules\VetClinic\Entities\VcAppointment::class);

            $context = $this->buildContext($businessId, $inbound, $searchText);
            if ($canBook) {
                $context .= "\n\n" . $this->petsContext($businessId, (string) $inbound->wa_id);
            }

            // Whitelisted tools for this turn, resolved from the shared registry.
            $tools = [];
            if ($canBook) {
                $registry = app(\Modules\AiCore\Entities\Services\AiToolRegistry::class);
                $registry->register(new Tools\AppointmentBookingTool());
                $tools = $registry->only(['book_appointment']);
            }

            // The system prompt is built by AiCore from the business's chosen
            // vertical (persona + grounding + security) plus the tool protocol.
            // WhatsApp adds only the channel-style hint.
            $system = \Modules\AiCore\Entities\Services\PromptBuilder::system($settings, [
                'business_name' => $businessName,
                'tools'         => $tools,
                'extra'         => trim($this->personaExtra($journey) . ' ' . $this->styleAndRules()),
            ]);

            $user = \Modules\AiCore\Entities\Services\PromptBuilder::userEnvelope($question, $context);

            $result = (new \Modules\AiCore\Entities\Services\ToolRunner())->run(
                $settings,
                $system,
                $user,
                $tools,
                ['wa_id' => (string) $inbound->wa_id, 'conversation' => $conversation],
                ['module' => 'whatsapp', 'feature' => 'auto_reply']
            );

            // An empty reply means the call failed (chat error / budget) — hand
            // off to staff. A non-empty reply (plain answer, booking confirmation
            // or a clarifying question from a tool) is sent as-is.
            $reply = trim((string) ($result['reply'] ?? ''));
            if ($reply === '') {
                Log::warning('WhatsApp AI reply failed: ' . ($result['error'] ?? 'unknown'));
                $this->reply($conversation, "Thanks for your message! A team member will get back to you shortly. 🙏");
                return;
            }

            if (!empty($result['meta']['label'])) {
                $this->addLabel($conversation, $result['meta']['label']);
            }

            $this->reply($conversation, $reply);
        } catch (\Throwable $e) {
            Log::warning('WaAiResponder error: ' . $e->getMessage());
        }
    }

    private function addLabel(WaConversation $conversation, string $label): void
    {
        $labels = $conversation->labels ?? [];
        if (!in_array($label, $labels, true)) {
            $labels[] = $label;
            $conversation->forceFill(['labels' => $labels])->save();
        }
    }

    /** Lists the customer's own pets so the model can fill a valid pet name. */
    private function petsContext(int $businessId, string $waId): string
    {
        if (!class_exists(\Modules\VetClinic\Entities\VcPet::class)) {
            return '';
        }
        $national = preg_replace('/^20/', '', $waId);
        $contactId = DB::table('contacts')->where('business_id', $businessId)
            ->where('mobile', 'like', "%{$national}%")->value('id');
        if (!$contactId) {
            return "Customer's pets on file: (none).";
        }
        $pets = \Modules\VetClinic\Entities\VcPet::where('business_id', $businessId)
            ->where('contact_id', $contactId)->pluck('petname')->all();

        return $pets ? "Customer's pets on file: " . implode(', ', $pets) . '.' : "Customer's pets on file: (none).";
    }

    private function reply(WaConversation $conversation, string $body): void
    {
        (new WaSender())->send([
            'business_id' => $conversation->business_id,
            'channel_id'  => $conversation->channel_id,
            'to'          => $conversation->wa_id,
            'body'        => $body,
            'source'      => 'ai',
        ]);
    }

    /** Personalisation instruction derived from the customer journey. */
    private function personaExtra(array $journey): string
    {
        $name = $journey['name'] ?? null;

        if (!empty($journey['just_named']) && $name) {
            return "The customer just shared their name: {$name}. Thank them warmly by name, then answer their question below.";
        }
        if ($name && !empty($journey['registered'])) {
            return "You are speaking with {$name}, a registered customer. Greet them warmly by name (e.g. \"Hi {$name}!\") on your first reply, then help.";
        }
        if ($name) {
            return "You are speaking with {$name} (a guest). Address them by name.";
        }
        return '';
    }

    /** Channel style + the product-availability + guided-discovery rules. */
    private function styleAndRules(): string
    {
        return 'Reply like a WhatsApp chat: short, warm, no markdown headings. '
            . 'PRODUCTS — confirm availability: when the customer asks about a product, use ONLY the CONTEXT product list. '
            . 'Clearly state whether it is IN STOCK (give the price, the branch/location and the quantity available) or that it is currently unavailable. '
            . 'Never promise stock that is not in the context. '
            . 'GUIDE THE CUSTOMER (they may not be technical): if their request is vague — e.g. just "a battery", "a charger", "ink" — without a specific model or part number, do NOT guess. '
            . 'Ask ONE friendly question for the exact model or part/SKU number, and tell them where to find it: it is usually printed on a sticker/label on the item itself (a code like "PT434"), on the original box, or in the device’s settings/manual. '
            . 'Also offer: they can simply send a PHOTO of the item or its label and you will read the details from it. '
            . 'Once you have a model / part number / clear identifier, search the CONTEXT and confirm availability. '
            . 'If the EXACT item is not in stock but the CONTEXT has similar/related products (e.g. the same brand or category), briefly offer those as alternatives and ask if any would suit. '
            . 'If nothing relevant is found at all, say you will check with the team.';
    }

    /** Assemble grounding facts: matching products in stock + the customer's appointments. */
    private function buildContext(int $businessId, WaMessage $inbound, string $text): string
    {
        $parts = [];

        $products = $this->searchProducts($businessId, $text);
        $parts[] = $products
            ? "Matching products (name | price | qty available | location):\n" . $products
            : "No matching products found in stock for this query.";

        $appointments = $this->appointmentsForWaId($businessId, (string) $inbound->wa_id);
        if ($appointments) {
            $parts[] = "Customer's upcoming appointments:\n" . $appointments;
        }

        return implode("\n\n", $parts);
    }

    /** Port of the wa_search product query: in-stock variations matching keywords. */
    private function searchProducts(int $businessId, string $text): string
    {
        $terms = array_slice(array_values(array_filter(
            preg_split('/[\s,]+/', $text, -1, PREG_SPLIT_NO_EMPTY),
            fn ($t) => mb_strlen($t) >= 3
        )), 0, 6);

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

        $rows = DB::table('variations as v')
            ->leftJoin('products as p', 'v.product_id', '=', 'p.id')
            ->leftJoin('variation_location_details as vld', 'v.id', '=', 'vld.variation_id')
            ->leftJoin('business_locations as bl', 'bl.id', '=', 'vld.location_id')
            ->where('p.business_id', $businessId)
            ->where('p.not_for_selling', 0)
            ->where('vld.qty_available', '>', 0)
            ->where(function ($q) use ($terms) {
                foreach ($terms as $term) {
                    $q->orWhere('p.name', 'like', "%{$term}%")
                      ->orWhere('p.product_description', 'like', "%{$term}%")
                      ->orWhere('p.product_custom_field1', 'like', "%{$term}%")
                      ->orWhere('p.product_custom_field3', 'like', "%{$term}%");
                }
            })
            ->groupBy('v.id', 'vld.location_id')
            ->limit(8)
            ->get([
                DB::raw('IF(v.name = "DUMMY", p.name, CONCAT(p.name, " + ", v.name)) as product_name'),
                'v.sell_price_inc_tax as price',
                'vld.qty_available as qty',
                'bl.name as location',
            ]);

        $lines = [];
        foreach ($rows as $r) {
            $lines[] = '- ' . $r->product_name . ' | ' . number_format((float) $r->price, 2)
                . ' | ' . (float) $r->qty . ' | ' . ($r->location ?? '');
        }
        return implode("\n", $lines);
    }

    /** Upcoming VetClinic appointments for the contact behind this number. */
    private function appointmentsForWaId(int $businessId, string $waId): string
    {
        if (!class_exists(\Modules\VetClinic\Entities\VcAppointment::class)) {
            return '';
        }

        // Match the ERP contact by the national part of the number.
        $national = preg_replace('/^20/', '', $waId);
        $contactIds = DB::table('contacts')
            ->where('business_id', $businessId)
            ->where('mobile', 'like', "%{$national}%")
            ->limit(5)->pluck('id')->all();

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

        $rows = DB::table('vc_appointments as a')
            ->leftJoin('vc_pets as p', 'p.id', '=', 'a.pet_id')
            ->leftJoin('vc_doctors as d', 'd.id', '=', 'a.doctor_id')
            ->where('a.business_id', $businessId)
            ->whereIn('a.contact_id', $contactIds)
            ->whereDate('a.appointment_date', '>=', now()->toDateString())
            ->whereNotIn('a.status', ['cancelled', 'canceled'])
            ->orderBy('a.appointment_date')
            ->limit(5)
            ->get(['a.appointment_date', 'a.appointment_time', 'p.petname', 'd.name as doctor', 'a.status']);

        $lines = [];
        foreach ($rows as $r) {
            $lines[] = '- ' . $r->petname . ' on ' . $r->appointment_date . ' ' . substr((string) $r->appointment_time, 0, 5)
                . ' with ' . ($r->doctor ?? 'the vet') . ' (' . $r->status . ')';
        }
        return implode("\n", $lines);
    }
}
