# @File: backend/services/whatsapp/ai.py
# @Desc: Client-side WhatsApp AI auto-reply service.
#        Provides read/write access to ``whatsapp_ai_settings`` for the
#        authenticated client's active subscription, plus a lightweight
#        ``generate_reply`` helper that routes into the existing Atoms
#        Cloud AI Hub (``services.aihub``) without introducing any new
#        model/provider dependencies. The heavy lifting of prompt shape
#        and tone stays in this module so the frontend only needs to
#        send ``{ incoming_message }`` when testing.
from __future__ import annotations

import logging
from datetime import datetime
from typing import Optional

from models.whatsapp import WhatsappAiSettings, WhatsappClientSubscription
from schemas.aihub import ChatMessage, GenTxtRequest
from services.aihub import AIHubService
from services.whatsapp.chat import _get_active_subscription
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

logger = logging.getLogger(__name__)

# -- Constants ---------------------------------------------------------------

# Atoms Cloud cost-effective text model — good enough for short chat replies.
DEFAULT_AI_MODEL = "deepseek-v3.2"

# Hard cap on knowledge-base characters sent to the model. Protects both the
# prompt budget and the request body.
MAX_KNOWLEDGE_BASE_CHARS = 6000
MAX_FAQ_CHARS = 4000
MAX_INCOMING_CHARS = 2000

ALLOWED_TONES = {"friendly", "professional", "concise", "casual"}
ALLOWED_LANGUAGES = {"en", "ar", "auto"}


def _truncate(text: Optional[str], limit: int) -> str:
    if not text:
        return ""
    text = text.strip()
    if len(text) <= limit:
        return text
    return text[:limit] + "…"


def _tone_instruction(tone: str) -> str:
    return {
        "friendly": "Keep the tone warm and friendly. Use light, natural language.",
        "professional": "Keep the tone professional, polite, and precise.",
        "concise": "Keep the reply very concise — one or two short sentences.",
        "casual": "Keep the tone casual and conversational.",
    }.get(tone, "Keep the tone friendly and helpful.")


def _language_instruction(language: str) -> str:
    if language == "en":
        return "Reply in English."
    if language == "ar":
        return "Reply in Modern Standard Arabic (العربية الفصحى)."
    # auto
    return "Reply in the same language the customer used in their message."


def _now_hhmm() -> str:
    return datetime.utcnow().strftime("%H:%M")


def _is_within_hours(now_hhmm: str, start: str, end: str) -> bool:
    """Return True if ``now_hhmm`` falls inside ``[start, end]``.

    Supports overnight windows where ``end`` <= ``start`` (e.g. 22:00 → 06:00).
    Invalid inputs fall back to "always open" to avoid blocking replies.
    """
    try:
        def _to_minutes(hhmm: str) -> int:
            h, m = hhmm.split(":", 1)
            return int(h) * 60 + int(m)

        n = _to_minutes(now_hhmm)
        s = _to_minutes(start)
        e = _to_minutes(end)
    except Exception:
        return True

    if s == e:
        return True
    if s < e:
        return s <= n <= e
    # overnight window
    return n >= s or n <= e


# -- Subscription helper -----------------------------------------------------


async def _require_active_subscription(
    db: AsyncSession, user_id: str
) -> WhatsappClientSubscription:
    sub = await _get_active_subscription(db, user_id)
    if not sub:
        raise LookupError("No active WhatsApp subscription for this user.")
    return sub


# -- Settings CRUD -----------------------------------------------------------


async def _load_or_create_settings(
    db: AsyncSession, subscription_id: int
) -> WhatsappAiSettings:
    stmt = select(WhatsappAiSettings).where(
        WhatsappAiSettings.subscription_id == subscription_id
    )
    row = (await db.execute(stmt)).scalar_one_or_none()
    if row is not None:
        return row

    row = WhatsappAiSettings(subscription_id=subscription_id)
    db.add(row)
    await db.flush()
    await db.refresh(row)
    return row


def _settings_to_dict(row: WhatsappAiSettings) -> dict:
    return {
        "subscription_id": row.subscription_id,
        "auto_reply_enabled": bool(row.auto_reply_enabled),
        "knowledge_base": row.knowledge_base or "",
        "faq_training": row.faq_training or "",
        "tone": row.tone or "friendly",
        "language": row.language or "en",
        "confidence_threshold": int(row.confidence_threshold or 60),
        "business_hours_only": bool(row.business_hours_only),
        "business_hours_start": row.business_hours_start or "09:00",
        "business_hours_end": row.business_hours_end or "18:00",
        "human_handoff_enabled": bool(row.human_handoff_enabled),
        "preview_before_sending": bool(row.preview_before_sending),
        "sentiment_detection_enabled": bool(row.sentiment_detection_enabled),
        "suggested_replies_enabled": bool(row.suggested_replies_enabled),
        "spam_detection_enabled": bool(row.spam_detection_enabled),
        "lead_qualification_enabled": bool(row.lead_qualification_enabled),
        "updated_at": row.updated_at.isoformat() if row.updated_at else None,
    }


async def get_settings(db: AsyncSession, user_id: str) -> dict:
    sub = await _require_active_subscription(db, user_id)
    row = await _load_or_create_settings(db, sub.id)
    await db.commit()
    return _settings_to_dict(row)


def _coerce_time(value: Optional[str], default: str) -> str:
    """Coerce a free-form string into ``HH:MM`` (24h). Falls back to default."""
    if not value:
        return default
    value = value.strip()
    # Accept HH:MM or H:MM
    parts = value.split(":")
    if len(parts) != 2:
        return default
    try:
        h = int(parts[0])
        m = int(parts[1])
    except ValueError:
        return default
    if not (0 <= h <= 23 and 0 <= m <= 59):
        return default
    return f"{h:02d}:{m:02d}"


async def update_settings(db: AsyncSession, user_id: str, payload: dict) -> dict:
    """Partial-update AI settings for the caller's active subscription.

    Unknown keys are ignored on purpose so older clients don't blow up when
    new toggles ship.
    """
    sub = await _require_active_subscription(db, user_id)
    row = await _load_or_create_settings(db, sub.id)

    if "auto_reply_enabled" in payload:
        row.auto_reply_enabled = bool(payload["auto_reply_enabled"])
    if "knowledge_base" in payload:
        row.knowledge_base = _truncate(payload.get("knowledge_base"), MAX_KNOWLEDGE_BASE_CHARS) or None
    if "faq_training" in payload:
        row.faq_training = _truncate(payload.get("faq_training"), MAX_FAQ_CHARS) or None
    if "tone" in payload:
        tone = str(payload.get("tone") or "").strip().lower()
        row.tone = tone if tone in ALLOWED_TONES else "friendly"
    if "language" in payload:
        lang = str(payload.get("language") or "").strip().lower()
        row.language = lang if lang in ALLOWED_LANGUAGES else "en"
    if "confidence_threshold" in payload:
        try:
            ct = int(payload["confidence_threshold"])
        except (TypeError, ValueError):
            ct = 60
        row.confidence_threshold = max(0, min(100, ct))
    if "business_hours_only" in payload:
        row.business_hours_only = bool(payload["business_hours_only"])
    if "business_hours_start" in payload:
        row.business_hours_start = _coerce_time(payload.get("business_hours_start"), "09:00")
    if "business_hours_end" in payload:
        row.business_hours_end = _coerce_time(payload.get("business_hours_end"), "18:00")
    if "human_handoff_enabled" in payload:
        row.human_handoff_enabled = bool(payload["human_handoff_enabled"])
    if "preview_before_sending" in payload:
        row.preview_before_sending = bool(payload["preview_before_sending"])
    if "sentiment_detection_enabled" in payload:
        row.sentiment_detection_enabled = bool(payload["sentiment_detection_enabled"])
    if "suggested_replies_enabled" in payload:
        row.suggested_replies_enabled = bool(payload["suggested_replies_enabled"])
    if "spam_detection_enabled" in payload:
        row.spam_detection_enabled = bool(payload["spam_detection_enabled"])
    if "lead_qualification_enabled" in payload:
        row.lead_qualification_enabled = bool(payload["lead_qualification_enabled"])

    await db.flush()
    await db.refresh(row)
    await db.commit()
    return _settings_to_dict(row)


# -- Preview / test reply ----------------------------------------------------


def _build_prompt(settings: WhatsappAiSettings, incoming: str) -> list[ChatMessage]:
    """Assemble the chat messages sent to the AI Hub.

    We intentionally keep this as two turns (system + user) so the behaviour
    is deterministic regardless of which upstream model handles the call.
    """
    system_lines = [
        "You are an AI auto-reply assistant for a business account on WhatsApp.",
        "You reply to a single incoming customer message. Never claim to be a human.",
        "Base every factual statement on the business knowledge below. "
        "If the information is missing, politely say you'll forward the question "
        "to a human teammate — do not invent details.",
        _tone_instruction(settings.tone or "friendly"),
        _language_instruction(settings.language or "en"),
        "Keep the reply short enough to be read comfortably inside a WhatsApp bubble (ideally under 80 words).",
    ]

    if settings.sentiment_detection_enabled:
        system_lines.append(
            "If the customer sounds frustrated, acknowledge the frustration briefly before answering."
        )
    if settings.spam_detection_enabled:
        system_lines.append(
            "If the message looks like spam or is completely unrelated to the business, "
            "reply with a single polite sentence declining to help."
        )
    if settings.lead_qualification_enabled:
        system_lines.append(
            "If the customer expresses buying intent, ask ONE short follow-up question "
            "to qualify the lead (budget, timeline, or product of interest)."
        )
    if settings.human_handoff_enabled:
        system_lines.append(
            "If the request needs human judgement (refund, dispute, escalation, "
            "legal, billing specifics), say a teammate will follow up shortly."
        )

    kb = _truncate(settings.knowledge_base, MAX_KNOWLEDGE_BASE_CHARS)
    faq = _truncate(settings.faq_training, MAX_FAQ_CHARS)

    knowledge_block = ""
    if kb:
        knowledge_block += f"\n\n--- BUSINESS KNOWLEDGE ---\n{kb}"
    if faq:
        knowledge_block += f"\n\n--- FAQ / EXAMPLES ---\n{faq}"
    if not knowledge_block:
        knowledge_block = "\n\n--- BUSINESS KNOWLEDGE ---\n(No knowledge base has been configured yet.)"

    system_prompt = "\n".join(system_lines) + knowledge_block

    user_message = _truncate(incoming, MAX_INCOMING_CHARS) or "(empty message)"

    return [
        ChatMessage(role="system", content=system_prompt),
        ChatMessage(role="user", content=f"Incoming customer message:\n{user_message}"),
    ]


async def generate_reply(
    db: AsyncSession,
    user_id: str,
    incoming_message: str,
    *,
    force: bool = False,
) -> dict:
    """Generate a preview auto-reply for ``incoming_message``.

    ``force=True`` bypasses the ``auto_reply_enabled`` and business-hours
    gates — this is used by the preview panel in the UI so the client can
    test the reply before flipping the main toggle. Returns a dict with:

    - ``reply`` – the generated reply (empty if the gate blocked the call).
    - ``gated`` – True if the reply was blocked by settings/hours.
    - ``reason`` – short human-readable explanation when gated.
    - ``model`` – model name used.
    - ``usage`` – token usage (when the upstream returns it).
    """
    sub = await _require_active_subscription(db, user_id)
    row = await _load_or_create_settings(db, sub.id)
    await db.commit()

    incoming = (incoming_message or "").strip()
    if not incoming:
        return {
            "reply": "",
            "gated": True,
            "reason": "Incoming message is empty.",
            "model": DEFAULT_AI_MODEL,
            "usage": None,
        }

    if not force:
        if not row.auto_reply_enabled:
            return {
                "reply": "",
                "gated": True,
                "reason": "Auto-reply is turned off.",
                "model": DEFAULT_AI_MODEL,
                "usage": None,
            }
        if row.business_hours_only and not _is_within_hours(
            _now_hhmm(), row.business_hours_start, row.business_hours_end
        ):
            return {
                "reply": "",
                "gated": True,
                "reason": "Outside configured business hours.",
                "model": DEFAULT_AI_MODEL,
                "usage": None,
            }

    messages = _build_prompt(row, incoming)
    service = AIHubService()
    try:
        resp = await service.gentxt(
            GenTxtRequest(
                model=DEFAULT_AI_MODEL,
                messages=messages,
                temperature=0.5,
                max_tokens=400,
            )
        )
    except Exception as exc:  # pragma: no cover — upstream failure path
        logger.warning("WhatsApp AI gentxt failed: %s", exc)
        return {
            "reply": "",
            "gated": True,
            "reason": f"AI service error: {exc}",
            "model": DEFAULT_AI_MODEL,
            "usage": None,
        }

    return {
        "reply": (resp.content or "").strip(),
        "gated": False,
        "reason": None,
        "model": resp.model or DEFAULT_AI_MODEL,
        "usage": resp.usage,
    }