"""
CA Automator — public landing-page chatbot.

Deliberately separate from ca_assistant.py (the internal, staff-only,
authenticated CA reasoning assistant grounded in rate_master + client data).
This module talks to anonymous website visitors, so its knowledge is
strictly limited to public site content (services, blog, careers, firm
contact details) — it must never see rate_master, client records, or any
other internal data, since there is no login boundary here at all.

Provider-agnostic by construction: every route and every piece of storage
(chatbot_conversation/chatbot_message) is provider-independent — the actual
AI call goes through ai_provider.py, the same shared dispatcher
ca_assistant.py (the internal assistant) uses. Swapping providers means
changing ai_provider.py, nothing here.

Guardrails (baked into the system prompt, not just described in a comment):
the assistant introduces CA-Automator, explains services, and guides
visitors to the right page/action, but never gives a confident, specific
tax/legal/accounting answer — those are always redirected to "book a
consultation," with the reasoning made visible to the visitor, not silently
refused.
"""
import re
import secrets

import ai_provider
import db

_MAX_TOKENS = 700          # a landing-page chat reply, not a reasoning essay
_MAX_MESSAGE_CHARS = 1500
_MAX_MESSAGES_PER_CONVERSATION = 40   # simple abuse guard on a public, unauthenticated endpoint


class ChatError(Exception):
    pass


def status():
    return ai_provider.status()


def new_session_token():
    return secrets.token_hex(16)


# --------------------------------------------------------------- site context
# Built fresh per request from live, published data only — never cached
# stale, never anything unpublished or internal. Small enough (a firm's
# service list is a few dozen rows at most) that a full scan per message is
# fine and easy to audit.

def _build_site_context(firm_id):
    firm = db.query("SELECT name, tagline, email, phone, address, office_hours FROM firm WHERE id = ?",
                     (firm_id,), one=True) or {}
    services = db.query(
        "SELECT name, summary, price_type, price, currency, is_buyable, requires_appointment"
        " FROM service WHERE firm_id = ? AND is_published = 1 ORDER BY sort_order LIMIT 30", (firm_id,))
    categories = db.query("SELECT name FROM blog_category ORDER BY name LIMIT 15")
    open_jobs = db.query(
        "SELECT title, location, employment_type FROM job_posting"
        " WHERE firm_id = ? AND is_published = 1 AND (deadline IS NULL OR deadline >= date('now'))"
        " ORDER BY created_at DESC LIMIT 10", (firm_id,))

    lines = ["<site_context>", f"Firm name: {firm.get('name') or 'CA Automator'}"]
    if firm.get("tagline"):
        lines.append(f"Tagline: {firm['tagline']}")
    if firm.get("email"):
        lines.append(f"Contact email: {firm['email']}")
    if firm.get("phone"):
        lines.append(f"Contact phone: {firm['phone']}")
    if firm.get("office_hours"):
        lines.append(f"Office hours: {firm['office_hours']}")

    if services:
        lines.append("\nPublished services (name — summary — price):")
        for s in services:
            price = ("Enquire for pricing" if s["price_type"] == "enquire"
                     else f"{'From ' if s['price_type'] == 'starting_from' else ''}{s['currency']} {s['price']:,.0f}"
                     if s["price"] is not None else "Enquire for pricing")
            lines.append(f"- {s['name']}: {s['summary'] or 'no summary on file'} ({price})")
    else:
        lines.append("\nNo services are currently published on the site.")

    if categories:
        lines.append("\nBlog categories available: " + ", ".join(c["name"] for c in categories))

    if open_jobs:
        lines.append("\nOpen positions:")
        for j in open_jobs:
            lines.append(f"- {j['title']} ({j['employment_type'] or 'not specified'}, {j['location'] or 'location not specified'})")
    else:
        lines.append("\nNo open positions are currently published.")

    lines.append("</site_context>")
    return "\n".join(lines)


_SYSTEM_PROMPT_TEMPLATE = """You are the website assistant for {firm_name}, a Chartered Accountancy \
practice using the CA-Automator platform. You greet visitors, explain what CA-Automator and this \
firm offer, and guide people to the right page or action. You are talking to an anonymous website \
visitor — you have no login, no access to any client's records, and no access to internal pricing \
rules or tax/legal databases beyond what is given to you below in <site_context>.

STRICT BOUNDARIES — read carefully, these are not optional:
1. You may confidently answer questions about CA-Automator itself and about this firm's published \
services, blog, careers and contact details — that information is given to you in <site_context> \
below and is safe to state plainly.
2. You may give brief, clearly-labelled GENERAL information about accounting/tax/compliance \
concepts (e.g. "VAT is a consumption tax" or "a company in Nepal generally needs to register with \
the Office of Company Registrar") — but always as general orientation, never as advice for the \
visitor's specific situation.
3. You must NEVER give a specific, confident answer to a question that depends on someone's actual \
facts — their numbers, their filings, their specific tax position, their specific legal situation, \
or anything that could be professional advice if wrong. For anything like this, say plainly that it \
needs a real consultation with the firm's CA team, and offer to help them book one. Do not hedge with \
a vague disclaimer buried in a long answer — say clearly "this needs a proper consultation" and mean it.
4. Before asking for any personal information (name, email, phone, company), briefly explain why you're \
asking (e.g. "so the team can follow up with you directly"). Never collect information the visitor \
hasn't been told the purpose of.
5. Keep replies short — a few sentences, landing-page chat, not an essay. Use plain text, no markdown \
headers. You may suggest one of these next steps when relevant: viewing services, booking a \
consultation, reading the blog, viewing careers, or contacting the firm directly.
6. If asked something entirely unrelated to CA-Automator, this firm, or general professional-services \
topics, answer briefly and steer back to how you can help with the firm's services.

<site_context>
{site_context}
</site_context>
"""


_PROVIDER_ERROR_MESSAGES = {
    "not_configured": "chat_not_configured",
    "invalid_api_key": "The chat assistant is misconfigured (invalid API key). Please use the contact form instead.",
    "rate_limited": "The chat assistant is a little busy right now — please try again in a moment.",
    "empty_response": "That took a bit long to think through — could you ask again, a little more briefly?",
    "unexpected_response": "The chat assistant returned an unexpected response — please try again.",
}


def _call_provider(messages, system):
    try:
        return ai_provider.call(messages, system, _MAX_TOKENS)["text"]
    except ai_provider.ProviderError as e:
        code = str(e)
        if code in _PROVIDER_ERROR_MESSAGES:
            raise ChatError(_PROVIDER_ERROR_MESSAGES[code])
        raise ChatError("The chat assistant hit an error — please try again or use the contact form.")


# ------------------------------------------------------------- conversation

def _get_or_create_conversation(firm_id, session_token, source="landing_page"):
    row = db.query(
        "SELECT * FROM chatbot_conversation WHERE firm_id = ? AND session_token = ? AND status != 'closed'"
        " ORDER BY id DESC LIMIT 1", (firm_id, session_token), one=True)
    if row:
        return dict(row)
    cid = db.execute(
        "INSERT INTO chatbot_conversation (firm_id, session_token, source) VALUES (?,?,?)",
        (firm_id, session_token, source))
    return dict(db.query("SELECT * FROM chatbot_conversation WHERE id = ?", (cid,), one=True))


def reply(firm_id, session_token, user_message, source="landing_page"):
    """Handles one visitor turn end to end: loads/creates the conversation,
    stores the user's message, calls the provider with full history +
    guardrail system prompt, stores and returns the assistant's reply.
    Raises ChatError with a message safe to show the visitor."""
    user_message = (user_message or "").strip()
    if not user_message:
        raise ChatError("Type a message first.")
    if len(user_message) > _MAX_MESSAGE_CHARS:
        raise ChatError(f"That message is too long — please keep it under {_MAX_MESSAGE_CHARS} characters.")

    conv = _get_or_create_conversation(firm_id, session_token, source)
    turn_count = db.query("SELECT COUNT(*) n FROM chatbot_message WHERE conversation_id = ?",
                          (conv["id"],), one=True)["n"]
    if turn_count >= _MAX_MESSAGES_PER_CONVERSATION:
        raise ChatError("This conversation has reached its length limit — please use the contact form to continue.")

    db.execute("INSERT INTO chatbot_message (conversation_id, role, content) VALUES (?,?,?)",
               (conv["id"], "user", user_message))

    history = db.query("SELECT role, content FROM chatbot_message WHERE conversation_id = ? ORDER BY id",
                       (conv["id"],))
    firm = db.query("SELECT name FROM firm WHERE id = ?", (firm_id,), one=True) or {}
    system = _SYSTEM_PROMPT_TEMPLATE.format(
        firm_name=firm.get("name") or "CA Automator",
        site_context=_build_site_context(firm_id))

    try:
        text = _call_provider([{"role": h["role"], "content": h["content"]} for h in history], system)
    except ChatError as e:
        if str(e) == "chat_not_configured":
            raise
        # A real provider error mid-conversation: don't leave the user's
        # message stranded with no reply in the transcript.
        db.execute("INSERT INTO chatbot_message (conversation_id, role, content) VALUES (?,?,?)",
                   (conv["id"], "assistant", f"[error — not shown to visitor as a normal reply] {e}"))
        raise

    db.execute("INSERT INTO chatbot_message (conversation_id, role, content) VALUES (?,?,?)",
               (conv["id"], "assistant", text))
    db.execute("UPDATE chatbot_conversation SET updated_at = datetime('now'),"
              " topic = COALESCE(topic, ?) WHERE id = ?", (user_message[:80], conv["id"]))
    return {"conversation_id": conv["id"], "reply": text}


_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")


def capture_lead(firm_id, session_token, name, email, phone, company, source="landing_page"):
    name = (name or "").strip()
    email = (email or "").strip()
    if not name or not email or not _EMAIL_RE.match(email):
        raise ChatError("A name and a valid email are required.")
    conv = _get_or_create_conversation(firm_id, session_token, source)
    db.execute(
        "UPDATE chatbot_conversation SET name = ?, email = ?, phone = ?, company = ?,"
        " status = 'needs_follow_up', updated_at = datetime('now') WHERE id = ?",
        (name, email, (phone or "").strip() or None, (company or "").strip() or None, conv["id"]))
    return conv["id"]
