#!/usr/bin/env python3
"""
provision_telnyx_assistant.py
=====================================
Standalone script to provision a Telnyx AI Assistant for a TradiesVoice client.

Extracted from: widget_per_client_provisioning.md v1.0.0
Version:        1.1.0 (production-ready runner)

USAGE — Inline flags:
    python provision_telnyx_assistant.py \\
        --sub-account-id "abc123" \\
        --business-name "Thornton Plumbing" \\
        --services "Plumbing, hot water, gas fitting, drain clearing" \\
        --service-area "Brisbane North and inner suburbs" \\
        --hours "Mon–Fri 7am–5pm, Sat 8am–12pm" \\
        --booking-link "https://thorntonplumbing.com.au/book" \\
        --escalation-number "+61412345678" \\
        --owner-name "Dave Thornton" \\
        --area-code "07" \\
        --widget-color "#1a1a2e" \\
        --provision-number

USAGE — JSON client data file:
    python provision_telnyx_assistant.py --client-data path/to/client.json

CLIENT DATA JSON FORMAT:
    {
        "sub_account_id": "abc123",
        "business_name": "Thornton Plumbing",
        "services": "Plumbing, hot water, gas fitting, drain clearing",
        "service_area": "Brisbane North and inner suburbs",
        "hours": "Mon–Fri 7am–5pm, Sat 8am–12pm",
        "booking_link": "https://thorntonplumbing.com.au/book",
        "escalation_number": "+61412345678",
        "owner_name": "Dave Thornton",
        "area_code": "07",
        "widget_color": "#1a1a2e",
        "provision_number": false,
        "existing_phone_number_id": null
    }

ENVIRONMENT VARIABLES REQUIRED:
    TELNYX_API_KEY         — Telnyx API key (write permissions on AI Assistants)
    GHL_AGENCY_API_KEY     — GHL Agency API key (for storing assistant ID in sub-account)

OUTPUT:
    Prints the Telnyx assistant ID and the widget embed code.
    Saves a per-client provisioning record to:
        GHL_MODULES/mctb_widget/provisioned_clients.jsonl
"""

import argparse
import io
import json
import os
import sys
import textwrap
from datetime import datetime, timezone
from pathlib import Path

import requests

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------

TELNYX_API_KEY = os.environ.get("TELNYX_API_KEY")
GHL_AGENCY_API_KEY = os.environ.get("GHL_AGENCY_API_KEY")

TELNYX_BASE = "https://api.telnyx.com/v2"
GHL_BASE = "https://services.leadconnectorhq.com"
GHL_API_VERSION = "2021-07-28"

# Record file — append one JSON line per provisioned client
PROVISIONING_LOG = Path(__file__).parent / "provisioned_clients.jsonl"


# ---------------------------------------------------------------------------
# Step 1: Create Telnyx AI Assistant
# ---------------------------------------------------------------------------

def create_telnyx_assistant(
    business_name: str,
    services: str,
    service_area: str,
    hours: str,
    booking_instructions: str,
    escalation_number: str,
) -> dict:
    """
    Create a new Telnyx AI Assistant for a client.
    Returns the full assistant API response including the new assistant ID.
    """
    if not TELNYX_API_KEY:
        raise RuntimeError("TELNYX_API_KEY environment variable not set.")

    headers = {
        "Authorization": f"Bearer {TELNYX_API_KEY}",
        "Content-Type": "application/json",
    }

    system_prompt = textwrap.dedent(f"""\
        You are AIVA, the AI receptionist for {business_name}. Your role is to answer \
        questions about the business, qualify callers as genuine leads, and direct them \
        to book an appointment or leave their details for a callback.

        Business details:
        - Name: {business_name}
        - Services: {services}
        - Service area: {service_area}
        - Trading hours: {hours}
        - Booking: {booking_instructions}

        Your tone is warm, professional, and clear. You are confident and helpful. \
        You never use slang or informal language. You speak naturally as an Australian \
        professional would.

        If you cannot answer a question or the caller needs urgent assistance, \
        transfer the call to: {escalation_number}.

        Never disclose that you are an AI unless directly asked. If asked, acknowledge \
        it honestly and continue to assist.""")

    payload = {
        "name": f"AIVA — {business_name}",
        "voice": {
            "voice_id": "en-AU-WilliamNeural",
            "provider": "azure",
        },
        "first_message": (
            f"Thank you for calling {business_name}. "
            "You're speaking with AIVA, the virtual receptionist. "
            "How can I help you today?"
        ),
        "system_prompt": system_prompt,
        "tools": [
            {
                "type": "transfer_call",
                "config": {
                    "destination": escalation_number,
                    "trigger_phrases": [
                        "speak to someone",
                        "talk to a person",
                        "human",
                        "real person",
                        "speak to the team",
                    ],
                },
            }
        ],
        "max_duration_seconds": 600,
        "end_call_after_silence_seconds": 20,
    }

    print(f"  -> POST {TELNYX_BASE}/ai/assistants")
    response = requests.post(
        f"{TELNYX_BASE}/ai/assistants",
        headers=headers,
        json=payload,
        timeout=30,
    )

    if response.status_code not in (200, 201):
        raise RuntimeError(
            f"Failed to create Telnyx AI Assistant: "
            f"HTTP {response.status_code}\n{response.text}"
        )

    data = response.json()
    assistant_id = data.get("data", {}).get("id")
    if not assistant_id:
        raise RuntimeError(f"Unexpected API response — no assistant ID: {data}")

    print(f"  [OK] Assistant created: {assistant_id}")
    return data


# ---------------------------------------------------------------------------
# Step 2: Upload Knowledge Base
# ---------------------------------------------------------------------------

def build_knowledge_base_text(
    business_name: str,
    services: str,
    service_area: str,
    hours: str,
    booking_link: str,
    owner_name: str,
    escalation_number: str,
    extra_faqs: list[dict] | None = None,
) -> str:
    """
    Build a structured plain-text knowledge base document for the assistant.
    extra_faqs: list of {"q": "question text", "a": "answer text"}
    """
    today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")

    faq_block = ""
    if extra_faqs:
        lines = []
        for faq in extra_faqs:
            lines.append(f"Q: {faq['q']}\nA: {faq['a']}")
        faq_block = "\n\n".join(lines)
    else:
        faq_block = (
            "Q: Do you provide free quotes?\n"
            "A: Yes, we provide free on-site quotes for most jobs.\n\n"
            "Q: How quickly can you attend?\n"
            "A: We aim to attend within 24 hours for standard jobs. "
            "Emergency callouts are available — call us directly."
        )

    return textwrap.dedent(f"""\
        BUSINESS KNOWLEDGE BASE — {business_name}
        Generated: {today}

        == SERVICES ==
        {services}

        == SERVICE AREA ==
        {service_area}

        == TRADING HOURS ==
        {hours}

        == BOOKING ==
        Book online at {booking_link} or we can arrange a time by phone.

        == FREQUENTLY ASKED QUESTIONS ==
        {faq_block}

        == ESCALATION ==
        If AIVA cannot assist, transfer the call to: {escalation_number}
        Manager / Owner: {owner_name}
        """)


def upload_knowledge_base(assistant_id: str, knowledge_text: str) -> dict:
    """
    Upload a knowledge base document to a Telnyx AI Assistant (multipart POST).
    """
    if not TELNYX_API_KEY:
        raise RuntimeError("TELNYX_API_KEY environment variable not set.")

    headers = {
        "Authorization": f"Bearer {TELNYX_API_KEY}",
    }

    file_content = knowledge_text.encode("utf-8")
    files = {
        "file": ("knowledge_base.txt", io.BytesIO(file_content), "text/plain"),
    }

    url = f"{TELNYX_BASE}/ai/assistants/{assistant_id}/documents"
    print(f"  -> POST {url}")
    response = requests.post(
        url,
        headers=headers,
        files=files,
        timeout=30,
    )

    if response.status_code not in (200, 201):
        raise RuntimeError(
            f"Failed to upload knowledge base: "
            f"HTTP {response.status_code}\n{response.text}"
        )

    print(f"  [OK] Knowledge base uploaded to assistant: {assistant_id}")
    return response.json()


# ---------------------------------------------------------------------------
# Step 3a (optional): Search and provision an Australian phone number
# ---------------------------------------------------------------------------

def provision_australian_number(area_code: str = "07") -> dict:
    """
    Search for an available Australian number and order it.
    area_code examples: "02" (NSW/ACT), "03" (VIC/TAS), "07" (QLD), "08" (WA/SA/NT)
    Returns the order response from Telnyx.
    """
    if not TELNYX_API_KEY:
        raise RuntimeError("TELNYX_API_KEY environment variable not set.")

    headers = {
        "Authorization": f"Bearer {TELNYX_API_KEY}",
        "Content-Type": "application/json",
    }

    # Strip leading zero for Telnyx national_destination_code filter
    ndc = area_code.lstrip("0") if area_code.startswith("0") else area_code

    search_url = f"{TELNYX_BASE}/available_phone_numbers"
    print(f"  -> GET {search_url} (country=AU, ndc={ndc})")
    search_response = requests.get(
        search_url,
        headers=headers,
        params={
            "filter[country_code]": "AU",
            "filter[national_destination_code]": ndc,
            "filter[features][]": "voice",
            "filter[limit]": 5,
        },
        timeout=30,
    )

    numbers = search_response.json().get("data", [])
    if not numbers:
        raise RuntimeError(
            f"No available AU numbers found for area code {area_code} (NDC: {ndc}). "
            "Try a different area code or provision manually in the Telnyx portal."
        )

    chosen_number = numbers[0]["phone_number"]
    print(f"  [INFO] Selected number: {chosen_number}")

    order_url = f"{TELNYX_BASE}/number_orders"
    print(f"  -> POST {order_url}")
    order_response = requests.post(
        order_url,
        headers=headers,
        json={"phone_numbers": [{"phone_number": chosen_number}]},
        timeout=30,
    )

    if order_response.status_code not in (200, 201):
        raise RuntimeError(
            f"Failed to order number {chosen_number}: "
            f"HTTP {order_response.status_code}\n{order_response.text}"
        )

    order_data = order_response.json()
    print(f"  [OK] Phone number ordered: {chosen_number}")
    return order_data


# ---------------------------------------------------------------------------
# Step 3b: Assign an existing or newly provisioned phone number to the assistant
# ---------------------------------------------------------------------------

def assign_number_to_assistant(phone_number_id: str, assistant_id: str) -> dict:
    """
    Route a Telnyx phone number to a Telnyx AI Assistant.
    phone_number_id: The Telnyx internal ID of the phone number (not the E.164 string).
    """
    if not TELNYX_API_KEY:
        raise RuntimeError("TELNYX_API_KEY environment variable not set.")

    headers = {
        "Authorization": f"Bearer {TELNYX_API_KEY}",
        "Content-Type": "application/json",
    }

    url = f"{TELNYX_BASE}/phone_numbers/{phone_number_id}"
    print(f"  -> PATCH {url}")
    response = requests.patch(
        url,
        headers=headers,
        json={"connection_id": assistant_id},
        timeout=30,
    )

    if response.status_code not in (200, 201):
        raise RuntimeError(
            f"Failed to assign number {phone_number_id} to assistant {assistant_id}: "
            f"HTTP {response.status_code}\n{response.text}"
        )

    print(f"  [OK] Number {phone_number_id} assigned to assistant {assistant_id}")
    return response.json()


# ---------------------------------------------------------------------------
# Step 4: Store assistant ID in GHL sub-account custom value
# ---------------------------------------------------------------------------

def store_assistant_id_in_ghl(
    sub_account_id: str,
    assistant_id: str,
) -> None:
    """
    Write the Telnyx assistant ID to the GHL sub-account as a custom value.
    Custom field name: telnyx_assistant_id
    Uses GHL_AGENCY_API_KEY from environment.
    """
    if not GHL_AGENCY_API_KEY:
        print(
            "  [WARN] GHL_AGENCY_API_KEY not set — skipping GHL storage.\n"
            f"         Store manually: telnyx_assistant_id = {assistant_id}"
        )
        return

    headers = {
        "Authorization": f"Bearer {GHL_AGENCY_API_KEY}",
        "Content-Type": "application/json",
        "Version": GHL_API_VERSION,
    }

    url = f"{GHL_BASE}/locations/{sub_account_id}/customValues"
    payload = {"name": "telnyx_assistant_id", "value": assistant_id}

    print(f"  -> POST {url}")
    response = requests.post(url, headers=headers, json=payload, timeout=30)

    if response.status_code in (200, 201):
        print(f"  [OK] Assistant ID stored in GHL sub-account: {sub_account_id}")
    elif response.status_code in (409, 422):
        print(f"  [WARN] Custom value already exists — attempting update...")
        _update_ghl_custom_value(sub_account_id, assistant_id, headers)
    else:
        print(
            f"  [WARN] Could not store assistant ID in GHL "
            f"(HTTP {response.status_code}).\n"
            f"         Store manually: telnyx_assistant_id = {assistant_id}\n"
            f"         Response: {response.text}"
        )


def _update_ghl_custom_value(
    sub_account_id: str,
    assistant_id: str,
    headers: dict,
) -> None:
    """
    Fetch the existing telnyx_assistant_id custom value and PATCH it.
    Called as a fallback from store_assistant_id_in_ghl on 409/422.
    """
    list_url = f"{GHL_BASE}/locations/{sub_account_id}/customValues"
    list_resp = requests.get(list_url, headers=headers, timeout=30)

    if list_resp.status_code != 200:
        print(
            f"  [WARN] Could not retrieve custom values to find existing ID. "
            f"Update manually: telnyx_assistant_id = {assistant_id}"
        )
        return

    custom_values = list_resp.json().get("customValues", [])
    target = None
    for cv in custom_values:
        if cv.get("name") == "telnyx_assistant_id":
            target = cv
            break

    if not target:
        print(
            f"  [WARN] Could not find existing telnyx_assistant_id custom value. "
            f"Update manually in GHL: {assistant_id}"
        )
        return

    cv_id = target.get("id")
    patch_url = f"{GHL_BASE}/locations/{sub_account_id}/customValues/{cv_id}"
    print(f"  -> PUT {patch_url}")
    patch_resp = requests.put(
        patch_url,
        headers=headers,
        json={"name": "telnyx_assistant_id", "value": assistant_id},
        timeout=30,
    )

    if patch_resp.status_code in (200, 201):
        print(f"  [OK] Updated existing telnyx_assistant_id → {assistant_id}")
    else:
        print(
            f"  [WARN] PUT failed (HTTP {patch_resp.status_code}). "
            f"Update manually: telnyx_assistant_id = {assistant_id}"
        )


# ---------------------------------------------------------------------------
# Step 5: Generate widget embed code
# ---------------------------------------------------------------------------

def generate_widget_embed_code(
    assistant_id: str,
    position: str = "bottom-right",
    primary_color: str = "#1a1a2e",
) -> str:
    """
    Generate the minimal <telnyx-ai-agent> embed snippet for the client's website.
    position: "bottom-right" or "bottom-left"
    primary_color: hex colour for the widget button
    """
    if position == "bottom-left":
        position_css = "position: fixed; bottom: 20px; left: 20px;"
    else:
        position_css = "position: fixed; bottom: 20px; right: 20px;"

    return f"""<!-- AIVA Talking Widget — ReceptionistAI -->
<style>
  telnyx-ai-agent {{
    --telnyx-color-primary: {primary_color};
  }}
</style>
<telnyx-ai-agent
  agent-id="{assistant_id}"
  style="{position_css} z-index: 9999;">
</telnyx-ai-agent>
<script async src="https://unpkg.com/@telnyx/ai-agent-widget"></script>"""


# ---------------------------------------------------------------------------
# Full provisioning orchestrator
# ---------------------------------------------------------------------------

def provision_client_widget(
    sub_account_id: str,
    business_name: str,
    services: str,
    service_area: str,
    hours: str,
    booking_link: str,
    escalation_number: str,
    owner_name: str,
    area_code: str = "07",
    widget_color: str = "#1a1a2e",
    provision_number: bool = False,
    existing_phone_number_id: str | None = None,
    extra_faqs: list[dict] | None = None,
) -> tuple[str, str]:
    """
    Full end-to-end provisioning for one client's AIVA Talking Widget.

    Returns:
        (assistant_id, embed_code)
    """
    started_at = datetime.now(tz=timezone.utc).isoformat()
    print(f"\n{'='*60}")
    print(f"  AIVA Widget Provisioning: {business_name}")
    print(f"  Started: {started_at}")
    print(f"{'='*60}\n")

    # --- Step 1: Create AI Assistant ---
    print("Step 1: Creating Telnyx AI Assistant...")
    booking_instructions = (
        f"Book online at {booking_link} or we can arrange a time by phone"
    )
    assistant_data = create_telnyx_assistant(
        business_name=business_name,
        services=services,
        service_area=service_area,
        hours=hours,
        booking_instructions=booking_instructions,
        escalation_number=escalation_number,
    )
    assistant_id: str = assistant_data["data"]["id"]

    # --- Step 2: Upload Knowledge Base ---
    print("\nStep 2: Uploading knowledge base...")
    kb_text = build_knowledge_base_text(
        business_name=business_name,
        services=services,
        service_area=service_area,
        hours=hours,
        booking_link=booking_link,
        owner_name=owner_name,
        escalation_number=escalation_number,
        extra_faqs=extra_faqs,
    )
    upload_knowledge_base(assistant_id, kb_text)

    # --- Step 3: Phone number (optional) ---
    if provision_number:
        print(f"\nStep 3: Provisioning AU phone number (area code: {area_code})...")
        order_data = provision_australian_number(area_code=area_code)
        # Extract phone number ID from order response for assignment
        ordered_numbers = (
            order_data.get("data", {})
            .get("phone_numbers", [])
        )
        if ordered_numbers:
            pn_id = ordered_numbers[0].get("id")
            if pn_id:
                print(f"\nStep 3b: Assigning number to assistant...")
                assign_number_to_assistant(pn_id, assistant_id)
            else:
                print(
                    "  [WARN] Could not extract phone number ID from order response. "
                    "Assign manually in Telnyx portal."
                )
        else:
            print(
                "  [WARN] Order response did not contain phone numbers. "
                "Check Telnyx portal to complete assignment."
            )
    elif existing_phone_number_id:
        print(f"\nStep 3: Assigning existing number {existing_phone_number_id} to assistant...")
        assign_number_to_assistant(existing_phone_number_id, assistant_id)
    else:
        print(
            "\nStep 3: Skipped (no --provision-number flag and no --phone-number-id given).\n"
            "         Assign a phone number to this assistant manually via Telnyx portal."
        )

    # --- Step 4: Store assistant ID in GHL ---
    print(f"\nStep 4: Storing assistant ID in GHL sub-account {sub_account_id}...")
    store_assistant_id_in_ghl(sub_account_id, assistant_id)

    # --- Step 5: Generate embed code ---
    embed_code = generate_widget_embed_code(
        assistant_id,
        primary_color=widget_color,
    )

    # --- Log to provisioned_clients.jsonl ---
    record = {
        "provisioned_at": started_at,
        "sub_account_id": sub_account_id,
        "business_name": business_name,
        "assistant_id": assistant_id,
        "area_code": area_code,
        "widget_color": widget_color,
        "provision_number": provision_number,
        "existing_phone_number_id": existing_phone_number_id,
        "escalation_number": escalation_number,
        "owner_name": owner_name,
    }
    with PROVISIONING_LOG.open("a", encoding="utf-8") as f:
        f.write(json.dumps(record) + "\n")
    print(f"\n  [OK] Record appended to: {PROVISIONING_LOG}")

    # --- Summary ---
    print(f"\n{'='*60}")
    print("  PROVISIONING COMPLETE")
    print(f"{'='*60}")
    print(f"  Business:     {business_name}")
    print(f"  Assistant ID: {assistant_id}")
    print(f"  Sub-account:  {sub_account_id}")
    print("\n--- Widget Embed Code (give to website deployer) ---\n")
    print(embed_code)
    print("\n" + "-" * 52)
    print("\nNEXT STEPS:")
    print("  1. Test call: dial the assigned phone number and confirm AIVA answers")
    print(f"  2. Check Telnyx portal: AI Assistants → {assistant_id}")
    print("  3. Hand embed code to website deployer (paste into <head> or before </body>)")
    print("  4. Verify call log appears in Telnyx portal after test call")
    print()

    return assistant_id, embed_code


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Provision a Telnyx AI Assistant (AIVA) for a TradiesVoice client.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )

    # Option A: JSON file
    parser.add_argument(
        "--client-data",
        metavar="PATH",
        help="Path to a JSON file containing all client fields.",
    )

    # Option B: Inline flags
    parser.add_argument("--sub-account-id", metavar="ID", help="GHL sub-account ID")
    parser.add_argument("--business-name", metavar="NAME", help="Client business name")
    parser.add_argument("--services", metavar="TEXT", help="Comma-separated list of services")
    parser.add_argument("--service-area", metavar="TEXT", help="Geographic service area")
    parser.add_argument("--hours", metavar="TEXT", help="Trading hours (e.g. 'Mon–Fri 7am–5pm')")
    parser.add_argument("--booking-link", metavar="URL", help="Online booking URL")
    parser.add_argument("--escalation-number", metavar="PHONE", help="E.164 escalation number")
    parser.add_argument("--owner-name", metavar="NAME", help="Owner / manager name")
    parser.add_argument(
        "--area-code", metavar="CODE", default="07",
        help="AU area code for number provisioning, e.g. 07 for QLD (default: 07)",
    )
    parser.add_argument(
        "--widget-color", metavar="HEX", default="#1a1a2e",
        help="Widget button hex colour (default: #1a1a2e)",
    )
    parser.add_argument(
        "--provision-number", action="store_true",
        help="Provision a new AU phone number and assign it to the assistant",
    )
    parser.add_argument(
        "--phone-number-id", metavar="ID",
        help="Existing Telnyx phone number ID to assign to the assistant",
    )

    return parser.parse_args()


def load_client_data(args: argparse.Namespace) -> dict:
    """Merge JSON file with inline CLI flags, CLI flags taking precedence."""
    data: dict = {}

    if args.client_data:
        path = Path(args.client_data)
        if not path.exists():
            sys.exit(f"[ERROR] Client data file not found: {path}")
        with path.open(encoding="utf-8") as f:
            data = json.load(f)

    # Override with any explicitly provided CLI flags
    flag_map = {
        "sub_account_id": args.sub_account_id,
        "business_name": args.business_name,
        "services": args.services,
        "service_area": args.service_area,
        "hours": args.hours,
        "booking_link": args.booking_link,
        "escalation_number": args.escalation_number,
        "owner_name": args.owner_name,
        "area_code": args.area_code,
        "widget_color": args.widget_color,
        "provision_number": args.provision_number or data.get("provision_number", False),
        "existing_phone_number_id": args.phone_number_id,
    }
    for key, val in flag_map.items():
        if val is not None:
            data[key] = val

    # Validate required fields
    required = [
        "sub_account_id", "business_name", "services", "service_area",
        "hours", "booking_link", "escalation_number", "owner_name",
    ]
    missing = [r for r in required if not data.get(r)]
    if missing:
        sys.exit(
            f"[ERROR] Missing required fields: {', '.join(missing)}\n"
            "Provide them via --client-data JSON or inline CLI flags."
        )

    return data


def main() -> None:
    args = parse_args()
    client = load_client_data(args)

    provision_client_widget(
        sub_account_id=client["sub_account_id"],
        business_name=client["business_name"],
        services=client["services"],
        service_area=client["service_area"],
        hours=client["hours"],
        booking_link=client["booking_link"],
        escalation_number=client["escalation_number"],
        owner_name=client["owner_name"],
        area_code=client.get("area_code", "07"),
        widget_color=client.get("widget_color", "#1a1a2e"),
        provision_number=client.get("provision_number", False),
        existing_phone_number_id=client.get("existing_phone_number_id"),
        extra_faqs=client.get("extra_faqs"),
    )


if __name__ == "__main__":
    main()
