"""Minimal Odoo XML-RPC client for the phone agent.

Creates an appointment *request* in Odoo from a captured call. A request is NOT a
confirmed booking — staff call the patient back to finalize — so by default we write
a CRM lead (a clean "to-do" that doesn't occupy a real calendar slot). Set
ODOO_TARGET=calendar to instead drop a tentative event on the calendar.

Auth + target are all env-driven (see .env.example). Connection is lazy and every
failure is swallowed by the caller's fallback, so a flaky Odoo never drops a request.
"""

import os
import xmlrpc.client
from datetime import datetime, timedelta
from html import escape

ODOO_URL = os.environ.get("ODOO_URL", "http://localhost:8069")
ODOO_DB = os.environ.get("ODOO_DB", "db1")
ODOO_USER = os.environ.get("ODOO_USER", "")
ODOO_API_KEY = os.environ.get("ODOO_API_KEY", "")
ODOO_TARGET = os.environ.get("ODOO_TARGET", "crm").lower()   # "crm" | "calendar"
# Pipeline placement for crm target. If ODOO_STAGE_ID is set, the request is created as a
# staged opportunity (shows up in the CRM pipeline as a worklist) instead of a bare lead.
ODOO_STAGE_ID = int(os.environ["ODOO_STAGE_ID"]) if os.environ.get("ODOO_STAGE_ID") else None
ODOO_TEAM_ID = int(os.environ["ODOO_TEAM_ID"]) if os.environ.get("ODOO_TEAM_ID") else None
ODOO_USER_ID = int(os.environ["ODOO_USER_ID"]) if os.environ.get("ODOO_USER_ID") else None


# Hard cap on any single XML-RPC round-trip. Without it, xmlrpc.client uses the OS
# default (no timeout): a HUNG Odoo (as opposed to a refused connection) would block
# post-call persistence forever — and since the call slot is released only after
# run_agent returns, two hung persists would leave the line permanently "busy".
ODOO_TIMEOUT_SECS = float(os.environ.get("ODOO_TIMEOUT_SECS", "15"))


class OdooError(RuntimeError):
    pass


class _TimeoutTransport(xmlrpc.client.Transport):
    def make_connection(self, host):
        conn = super().make_connection(host)
        conn.timeout = ODOO_TIMEOUT_SECS
        return conn


class _TimeoutSafeTransport(xmlrpc.client.SafeTransport):
    def make_connection(self, host):
        conn = super().make_connection(host)
        conn.timeout = ODOO_TIMEOUT_SECS
        return conn


def _proxy(path):
    transport = (_TimeoutSafeTransport() if ODOO_URL.lower().startswith("https")
                 else _TimeoutTransport())
    return xmlrpc.client.ServerProxy(f"{ODOO_URL}{path}", transport=transport)


def _connect():
    if not (ODOO_USER and ODOO_API_KEY):
        raise OdooError("ODOO_USER / ODOO_API_KEY not set")
    common = _proxy("/xmlrpc/2/common")
    uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_API_KEY, {})
    if not uid:
        raise OdooError("Odoo authentication failed (check db/user/key)")
    models = _proxy("/xmlrpc/2/object")
    return uid, models


def _exec(uid, models, model, method, *args, **kw):
    return models.execute_kw(ODOO_DB, uid, ODOO_API_KEY, model, method, list(args), kw)


def _find_or_create_partner(uid, models, name, phone):
    """Return a res.partner id, matching on phone first, else creating one."""
    domain = []
    if phone:
        domain = ["|", ["phone", "=", phone], ["mobile", "=", phone]]
    if domain:
        hit = _exec(uid, models, "res.partner", "search", domain, limit=1)
        if hit:
            return hit[0]
    vals = {"name": name or "Phone caller", "phone": phone or False, "company_type": "person"}
    return _exec(uid, models, "res.partner", "create", vals)


def create_appointment_request(patient_name, callback_number, reason, preferred_time,
                               insurance=None, call_sid=None, kind="appointment"):
    """Create the request in Odoo. `kind` is "appointment" or "callback" (a message for staff
    to call the caller back about something we can't do on the phone). Returns (model, id)."""
    uid, models = _connect()
    if kind == "callback":
        summary = f"📞 Callback request — {patient_name or 'caller'}" + (f": {reason}" if reason else "")
        header = "<b>Captured by the AVC phone agent</b> — CALL THE PATIENT BACK about this request."
        rows = [
            ("Name", patient_name),
            ("Callback", callback_number),
            ("What they need", reason),
            ("Twilio call SID", call_sid),
        ]
    else:
        summary = f"📞 Phone appt — {patient_name or 'caller'}" + (f": {reason}" if reason else "")
        header = "<b>Captured by the AVC phone agent</b> (UNCONFIRMED — call patient to finalize)."
        rows = [
            ("Name", patient_name),
            ("Callback", callback_number),
            ("Reason", reason),
            ("Insurance (log only — staff to verify coverage)", insurance),
            ("Preferred time (patient's words)", preferred_time),
            ("Twilio call SID", call_sid),
        ]
    # description is an Odoo HTML field — build with <br/> so it renders in the UI.
    note = f"<p>{header}</p><p>" + \
        "<br/>".join(f"{escape(k)}: {escape(str(v)) if v else '—'}" for k, v in rows) + "</p>"

    if ODOO_TARGET == "calendar":
        partner_id = _find_or_create_partner(uid, models, patient_name, callback_number)
        # Tentative 30-min slot tomorrow 9:00 as a visible placeholder; real time set on callback.
        start = (datetime.utcnow() + timedelta(days=1)).replace(hour=9, minute=0, second=0, microsecond=0)
        vals = {
            "name": summary,
            "start": start.strftime("%Y-%m-%d %H:%M:%S"),
            "stop": (start + timedelta(minutes=30)).strftime("%Y-%m-%d %H:%M:%S"),
            "description": note,
            "partner_ids": [(4, partner_id)],
        }
        rec = _exec(uid, models, "calendar.event", "create", vals)
        return ("calendar.event", rec)

    # CRM target. With a stage configured, create a staged opportunity (lands in the
    # pipeline as a worklist staff act on); otherwise a plain lead.
    vals = {
        "name": summary,
        "contact_name": patient_name or False,
        "phone": callback_number or False,
        "description": note,
        "type": "opportunity" if ODOO_STAGE_ID else "lead",
    }
    if ODOO_STAGE_ID:
        vals["stage_id"] = ODOO_STAGE_ID
    if ODOO_TEAM_ID:
        vals["team_id"] = ODOO_TEAM_ID
    if ODOO_USER_ID:
        vals["user_id"] = ODOO_USER_ID
    rec = _exec(uid, models, "crm.lead", "create", vals)
    return ("crm.lead", rec)


def attach_call_artifacts(model, rec_id, wav_path=None, transcript=None, call_sid=None):
    """Attach the call recording (WAV) and post the transcript to a lead's chatter.

    Called post-call for the record created by create_appointment_request. The transcript
    goes in as a chatter note (readable in place) AND as a .txt attachment (downloadable);
    the WAV is attached as-is. Any failure raises — the caller logs and moves on (the lead
    itself is already safe)."""
    import base64

    uid, models = _connect()
    sid = call_sid or "call"

    if transcript:
        body = "<b>📼 Call transcript</b><br/>" + "<br/>".join(
            escape(line) for line in transcript.splitlines() if line.strip()
        )
        _exec(uid, models, model, "message_post", [rec_id], body=body)
        _exec(uid, models, "ir.attachment", "create", [{
            "name": f"transcript_{sid}.txt",
            "datas": base64.b64encode(transcript.encode()).decode(),
            "res_model": model,
            "res_id": rec_id,
            "mimetype": "text/plain",
        }])

    if wav_path and os.path.isfile(wav_path):
        with open(wav_path, "rb") as fh:
            b64 = base64.b64encode(fh.read()).decode()
        _exec(uid, models, "ir.attachment", "create", [{
            "name": os.path.basename(wav_path),
            "datas": b64,
            "res_model": model,
            "res_id": rec_id,
            "mimetype": "audio/wav",
        }])
