"""Post-call appointment extraction.

Instead of unreliable in-call tool-calling (which made llama3.1:8b speak raw JSON),
we let the agent gather appointment details conversationally, then run ONE structured
extraction over the finished transcript and write it to Odoo. Reliable because it's a
single JSON-mode completion, not mid-conversation tool emission.
"""

import json
import re
from datetime import datetime

import httpx
from loguru import logger

from practice import persist_appointment

_EXTRACT_INSTRUCTIONS = (
    "You are reviewing a phone-call transcript between a caller and the receptionist "
    "for an optometry practice. Classify what the caller wanted and extract the details.\n"
    "Respond with ONLY a JSON object with these keys:\n"
    '  "request_type": one of "appointment", "callback", "none":\n'
    '      - "appointment": the caller asked to book/schedule a visit\n'
    '      - "callback": the caller wants something staff must handle off the phone — checking '
    "an existing order or purchase, whether frames/lenses are ready, a prescription or lab "
    "status, billing, an account lookup, or to reach a specific person\n"
    '      - "none": just a question that was answered, or nothing actionable\n'
    '  "patient_name": string or null — the caller\'s FULL name (first and last) if given\n'
    '  "phone_ok": boolean or null — true if the caller confirmed their caller-ID number is the '
    "best one, false if they said to use a different number, null if it never came up\n"
    '  "alternate_number": string or null — a different callback number the caller gave, digits only\n'
    '  "location": string or null (which office/city — appointments only)\n'
    '  "reason": string or null — for an APPOINTMENT, why they want to be seen: the visit type '
    "OR the eye problem/symptom they describe (e.g. \"annual eye exam\", \"blurry vision\", "
    '"vision loss", "eye pain", "broken glasses"). For a CALLBACK, a one-line note of what they '
    'need (e.g. "checking if frames ordered last week are ready", "question about a bill").\n'
    '  "insurance": string or null — the insurance plan the caller named, exactly as they said it\n'
    '  "preferred_time": string or null — the day/time in the caller\'s own words\n'
    '  "resolved_date": string or null — the actual calendar date the caller means as YYYY-MM-DD, '
    "computed from TODAY given below; null if it cannot be determined\n"
    "Use null for anything not clearly stated. Do not invent values."
)


def format_transcript(messages, agent_name="AVA") -> str:
    """Plain-text transcript of the call (Caller/agent turns) for chatter/attachments."""
    lines = []
    for m in messages:
        if m.get("role") not in ("user", "assistant"):
            continue
        content = m.get("content")
        if not isinstance(content, str) or not content.strip():
            continue
        who = "Caller" if m["role"] == "user" else agent_name
        lines.append(f"{who}: {content.strip()}")
    return "\n".join(lines)


async def extract_and_record(messages, ollama_url, model, call_sid=None, caller_number=None):
    """Extract an appointment from the transcript and persist it. Returns the record
    dict if one was saved, else None."""
    # Build a plain transcript from the conversation (skip the system prompt).
    turns = [
        f"{m['role']}: {m['content']}"
        for m in messages
        if m.get("role") in ("user", "assistant") and isinstance(m.get("content"), str) and m["content"].strip()
    ]
    if not any(m.get("role") == "user" for m in messages):
        return None  # nobody said anything
    transcript = "\n".join(turns)

    base = ollama_url.rstrip("/")
    if base.endswith("/v1"):
        base = base[:-3]

    today = datetime.now().strftime("%A, %B %d, %Y").replace(" 0", " ")
    try:
        async with httpx.AsyncClient(timeout=30) as client:
            r = await client.post(
                f"{base}/api/chat",
                json={
                    "model": model,
                    "format": "json",
                    "stream": False,
                    "options": {"temperature": 0},
                    "messages": [
                        {"role": "system", "content": f"{_EXTRACT_INSTRUCTIONS}\n\nTODAY is {today}."},
                        {"role": "user", "content": f"Transcript:\n{transcript}"},
                    ],
                },
            )
            r.raise_for_status()
            data = json.loads(r.json()["message"]["content"])
    except Exception:
        logger.exception("Appointment extraction call failed")
        return None

    kind = (data.get("request_type") or "none").strip().lower()
    if kind not in ("appointment", "callback"):
        logger.info("Post-call extraction: no actionable request")
        return None

    name = (data.get("patient_name") or "").strip()
    location = (data.get("location") or "").strip()
    reason_raw = (data.get("reason") or "").strip()
    # Lead-quality gate so staff get an actionable card:
    #  - appointment: need a NAME or a LOCATION
    #  - callback: need a NAME or a clear note of what they want
    if kind == "appointment" and not name and not location:
        logger.info("Post-call extraction: appointment intent but no name/location — skipping card")
        return None
    if kind == "callback" and not name and not reason_raw:
        logger.info("Post-call extraction: callback intent but no name/request note — skipping card")
        return None

    # Callback number: default to the verified Twilio caller-ID. If the caller explicitly
    # declined it (phone_ok == False) and gave a different number, use the one they gave.
    alt = (data.get("alternate_number") or "").strip()
    phone_ok = data.get("phone_ok")
    if phone_ok is False and alt:
        callback = alt
    else:
        callback = caller_number or alt or None
    # If they gave a different number but didn't clearly decline the caller-ID, keep it as a note.
    note_alt = (
        alt and caller_number and callback != alt
        and re.sub(r"\D", "", alt) != re.sub(r"\D", "", caller_number)
    )

    # Preferred time = caller's words, annotated with the resolved calendar date if we got one.
    preferred = (data.get("preferred_time") or "").strip() or None
    resolved = (data.get("resolved_date") or "").strip() or None
    if resolved:
        preferred = f"{preferred} (resolved: {resolved})" if preferred else resolved

    reason = data.get("reason")
    if note_alt:
        reason = f"{reason or ''} (caller also mentioned number: {alt})".strip()

    record = {
        "call_sid": call_sid,
        "kind": kind,  # "appointment" | "callback"
        "patient_name": data.get("patient_name"),
        "callback_number": callback,
        "phone_confirmed": phone_ok,
        "location": data.get("location") if kind == "appointment" else None,
        "reason": reason,
        "insurance": (data.get("insurance") or "").strip() or None if kind == "appointment" else None,
        "preferred_time": preferred if kind == "appointment" else None,
        "source": "post_call_extraction",
    }
    where = persist_appointment(record)
    record["persisted_to"] = where  # "odoo:<model>:<id>" | "jsonl" — used to attach artifacts
    logger.info(
        f"Post-call {kind} saved ({where}): {record['patient_name']} / "
        f"{record['location']} / reason={record['reason']} / ins={record['insurance']} / "
        f"when={record['preferred_time']}"
    )
    return record
