<?php

declare(strict_types=1);

namespace App\Services;

use App\Core\Database;
use App\Models\ConversationModel;
use App\Models\KnowledgeBaseModel;
use App\Models\LeadModel;

/**
 * ChatbotService
 *
 * The public website widget's brain. Every inbound visitor message
 * goes through handleMessage(), which — in order — checks for an
 * explicit human-handoff request, tries to capture contact info if the
 * message contains an email/phone, searches the Knowledge Base for an
 * FAQ match, and otherwise falls back to OpenAI (if configured) or a
 * safe "let me connect you with someone" message.
 *
 * Booking: if OpenAI is configured, natural-language booking requests
 * are handled by handing the message to AiAssistantService (same
 * tool-calling loop the internal Assistant uses — book_appointment is
 * one of its real tools). If OpenAI is NOT configured, the chatbot
 * still captures the request as a lead and flags it for a human to
 * schedule — never silently drops a booking request just because no
 * LLM is connected.
 */
final class ChatbotService
{
    private const ESCALATION_PHRASES = ['talk to a human', 'speak to someone', 'real person', 'agent please', 'representative'];
    private const BOOKING_PHRASES = ['book', 'appointment', 'schedule', 'reschedule'];

    public function __construct(
        private readonly ConversationModel $conversations = new ConversationModel(),
        private readonly KnowledgeBaseModel $knowledgeBase = new KnowledgeBaseModel(),
        private readonly LeadModel $leads = new LeadModel(),
        private readonly OpenAiClient $openAi = new OpenAiClient()
    ) {
    }

    public function startConversation(int $businessLineId, ?string $visitorIdentifier = null): array
    {
        $conversationId = $this->conversations->start('chatbot', $businessLineId, $visitorIdentifier);
        $greeting = $this->greetingFor($businessLineId);
        $this->conversations->addMessage($conversationId, 'bot', $greeting);

        return ['conversation_id' => $conversationId, 'message' => $greeting];
    }

    /** @return array{reply:string, escalated:bool, lead_captured:bool} */
    public function handleMessage(int $conversationId, string $visitorMessage, int $businessLineId): array
    {
        $this->conversations->addMessage($conversationId, 'visitor', $visitorMessage);
        $lower = mb_strtolower($visitorMessage);

        // 1. Explicit human handoff request.
        if ($this->matchesAny($lower, $this->handoffKeywords($businessLineId))) {
            $reply = "Of course — I'm connecting you with a member of our team now. Someone will be with you shortly.";
            $this->conversations->addMessage($conversationId, 'bot', $reply);
            $this->conversations->end($conversationId, 'escalated');
            $this->notifyStaffOfEscalation($conversationId, $businessLineId);

            return ['reply' => $reply, 'escalated' => true, 'lead_captured' => false];
        }

        // 2. Contact info in the message → capture as a lead ("Collect Contact Information" / "Capture Leads").
        $leadCaptured = false;
        $contact = $this->extractContact($visitorMessage);
        if ($contact['email'] !== null || $contact['phone'] !== null) {
            $leadCaptured = $this->captureLead($conversationId, $businessLineId, $contact, $visitorMessage);
        }

        // 3. Booking intent.
        if ($this->matchesAny($lower, self::BOOKING_PHRASES)) {
            $reply = $this->handleBookingIntent($conversationId, $visitorMessage, $businessLineId, $leadCaptured);
            $this->conversations->addMessage($conversationId, 'bot', $reply);

            return ['reply' => $reply, 'escalated' => false, 'lead_captured' => $leadCaptured];
        }

        // 4. FAQ via knowledge base — used as grounding context for a natural
        // AI-phrased answer when OpenAI is connected, or returned as-is
        // when it isn't (the best available answer without an LLM to
        // phrase it). This is the difference between "the same canned
        // sentence for every visitor" and a response that actually reads
        // their question and answers it conversationally while staying
        // grounded in real facts instead of the LLM guessing.
        $kbResults = $this->knowledgeBase->search($visitorMessage, $businessLineId, 3);
        if ($kbResults !== []) {
            $this->knowledgeBase->incrementViewCount((int) $kbResults[0]['id']);

            if ($this->openAi->isConfigured()) {
                $context = "Relevant facts from our knowledge base (use these to answer accurately, in your own natural words — do not just repeat them verbatim):\n";
                foreach ($kbResults as $article) {
                    $context .= "- Q: {$article['question']}\n  A: {$article['answer']}\n";
                }
                $reply = $this->askOpenAi($conversationId, $visitorMessage, $businessLineId, $context);
            } else {
                $reply = $kbResults[0]['answer'];
            }

            if ($leadCaptured) {
                $reply .= ' Thanks for sharing your contact info — a team member may follow up with more details.';
            }

            $this->conversations->addMessage($conversationId, 'bot', $reply);

            return ['reply' => $reply, 'escalated' => false, 'lead_captured' => $leadCaptured];
        }

        // 5. Fall back to OpenAI if configured, else a safe default.
        $reply = $this->openAi->isConfigured()
            ? $this->askOpenAi($conversationId, $visitorMessage, $businessLineId)
            : "I'm not sure about that one — I've let our team know you're looking for help, and someone will follow up soon. In the meantime, is there anything else I can help with?";

        $this->conversations->addMessage($conversationId, 'bot', $reply);

        return ['reply' => $reply, 'escalated' => false, 'lead_captured' => $leadCaptured];
    }

    private function handleBookingIntent(int $conversationId, string $visitorMessage, int $businessLineId, bool $leadJustCaptured): string
    {
        if ($this->openAi->isConfigured()) {
            return $this->askOpenAi($conversationId, $visitorMessage, $businessLineId);
        }

        $conversation = Database::connection()->prepare('SELECT related_lead_id FROM conversations WHERE id = :id');
        $conversation->execute(['id' => $conversationId]);
        $hasLead = (bool) $conversation->fetchColumn();

        if ($hasLead || $leadJustCaptured) {
            $this->conversations->end($conversationId, 'lead_created');

            return "Got it — I've noted you'd like to book an appointment. Please share your preferred day and time (or a phone number), and our team will confirm shortly.";
        }

        return "I'd love to help you book that. Could you share your name and email or phone number so our team can confirm the appointment with you?";
    }

    private function askOpenAi(int $conversationId, string $visitorMessage, int $businessLineId, ?string $groundingContext = null): string
    {
        $history = array_map(
            fn (array $m) => ['role' => $m['sender'] === 'visitor' ? 'user' : 'assistant', 'content' => $m['message']],
            $this->conversations->messages($conversationId)
        );

        $messageForAssistant = $groundingContext === null
            ? $visitorMessage
            : "{$groundingContext}\nVisitor's message: {$visitorMessage}";

        $assistant = new AiAssistantService($this->openAi);
        $result = $assistant->ask($messageForAssistant, array_slice($history, 0, -1), 0, $businessLineId);

        return $result['success']
            ? $result['reply']
            : "I'm having trouble answering that right now — I've let our team know, and someone will follow up soon.";
    }

    private function captureLead(int $conversationId, int $businessLineId, array $contact, string $rawMessage): bool
    {
        $duplicates = $this->leads->findPotentialDuplicates($contact['email'], $contact['phone']);
        if ($duplicates !== []) {
            $this->linkConversationToLead($conversationId, (int) $duplicates[0]['id']);

            return true;
        }

        $newStageId = Database::connection()->query("SELECT id FROM lead_stages WHERE slug = 'new' LIMIT 1")->fetchColumn();

        $leadId = $this->leads->insert([
            'business_line_id' => $businessLineId,
            'lead_stage_id' => (int) $newStageId,
            'first_name' => $contact['name'] ?? 'Website Visitor',
            'email' => $contact['email'],
            'phone' => $contact['phone'],
            'notes' => "Captured via chatbot: \"{$rawMessage}\"",
            'currency' => 'CAD',
        ]);

        $this->linkConversationToLead($conversationId, $leadId);
        ActivityLogger::record('lead', $leadId, 'lead_created', 'Lead captured via website chatbot');

        return true;
    }

    private function linkConversationToLead(int $conversationId, int $leadId): void
    {
        Database::connection()->prepare('UPDATE conversations SET related_lead_id = :lead WHERE id = :id')
            ->execute(['lead' => $leadId, 'id' => $conversationId]);
    }

    private function extractContact(string $message): array
    {
        $email = null;
        $phone = null;
        $name = null;

        if (preg_match('/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/', $message, $m)) {
            $email = $m[0];
        }
        if (preg_match('/(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/', $message, $m)) {
            $phone = preg_replace('/[^\d+]/', '', $m[0]);
        }
        if (preg_match('/my name is ([a-zA-Z\'-]+(?:\s[a-zA-Z\'-]+)?)/i', $message, $m)) {
            $name = trim($m[1]);
        }

        return ['email' => $email, 'phone' => $phone, 'name' => $name];
    }

    private function matchesAny(string $haystack, array $needles): bool
    {
        foreach ($needles as $needle) {
            if ($needle !== '' && str_contains($haystack, mb_strtolower($needle))) {
                return true;
            }
        }

        return false;
    }

    private function handoffKeywords(int $businessLineId): array
    {
        $stmt = Database::connection()->prepare('SELECT handoff_keywords FROM chatbot_settings WHERE business_line_id = :bl');
        $stmt->execute(['bl' => $businessLineId]);
        $raw = $stmt->fetchColumn();

        $configured = $raw ? array_map('trim', explode(',', $raw)) : [];

        return array_merge(self::ESCALATION_PHRASES, $configured);
    }

    private function greetingFor(int $businessLineId): string
    {
        $stmt = Database::connection()->prepare('SELECT greeting_message FROM chatbot_settings WHERE business_line_id = :bl');
        $stmt->execute(['bl' => $businessLineId]);
        $greeting = $stmt->fetchColumn();

        return $greeting ?: "Hi! I'm here to help with any questions about our services, or to help you book an appointment. What can I help with today?";
    }

    private function notifyStaffOfEscalation(int $conversationId, int $businessLineId): void
    {
        NotificationService::notifyBusinessLine(
            $businessLineId,
            'ai_escalation',
            'Chatbot escalation',
            "A website visitor asked to speak with a human (conversation #{$conversationId})."
        );
    }
}