#!/usr/bin/env python3
"""
tools/telnyx_client.py — Genesis Telnyx SDK Client
====================================================
SDK-first wrapper for all Telnyx AI assistant and phone number operations.
Replaces every raw curl/requests call in the codebase.

Venv:   source /mnt/e/genesis-system/.venv_telnyx/bin/activate
Usage:  python3 tools/telnyx_client.py <command> [options]

Commands:
  list                     List all assistants
  get <id>                 Get one assistant (full config)
  create                   Interactive create (prompts for fields)
  update <id> [--field v]  Update specific fields
  versions <id>            List versions of an assistant
  promote <id> <ver_id>    Promote a version to main
  numbers                  List your Telnyx phone numbers
  search                   Search available numbers (AU/US)
  audit                    Full system audit
  assign <num_id> <conn>   Assign number to a connection

Examples:
  python3 tools/telnyx_client.py list
  python3 tools/telnyx_client.py get assistant-9c42d3ce-e05a-4e34-8083-c91081917637
  python3 tools/telnyx_client.py search --country AU --area 07 --limit 5
  python3 tools/telnyx_client.py audit
"""

import os
import sys
import json
import argparse
from pathlib import Path
from typing import Optional

# ── Load secrets from secrets.env before importing SDK ──────────────────────
def _load_secrets() -> None:
    secrets_path = Path(__file__).parent.parent / "config" / "secrets.env"
    if secrets_path.exists():
        for line in secrets_path.read_text().splitlines():
            line = line.strip()
            if line and not line.startswith("#") and "=" in line:
                key, _, val = line.partition("=")
                key = key.strip()
                val = val.strip().strip('"').strip("'")
                if key and val and key not in os.environ:
                    os.environ[key] = val

_load_secrets()

# ── SDK import (requires .venv_telnyx activated or sys.path patched) ─────────
VENV_SITE = Path(__file__).parent.parent / ".venv_telnyx/lib/python3.12/site-packages"
if VENV_SITE.exists() and str(VENV_SITE) not in sys.path:
    sys.path.insert(0, str(VENV_SITE))

try:
    from telnyx import Telnyx
except ImportError:
    print("ERROR: Telnyx SDK not found. Activate .venv_telnyx or run:")
    print("  source /mnt/e/genesis-system/.venv_telnyx/bin/activate")
    sys.exit(1)


# ── Known Genesis assistants (registry) ──────────────────────────────────────
KNOWN_ASSISTANTS = {
    "aiva":           "assistant-696799a5-e994-4ac1-8f26-7b0923aee682",
    "aiva-v2":        "assistant-991f3950-12fd-4e90-a728-4149c16524e0",
    "voice-terminal": "assistant-cffc79bc-fd3b-4f96-a8e1-31a360100eb5",
    "voice-terminal2":"assistant-233d7403-044b-4b95-8eef-3554186306cc",
    "george":         "assistant-6f6f4ca2-3155-4930-95cb-27f59514af3e",
    "growth-director":"assistant-c0265496-86a2-46fd-a932-4c4398009013",
    "agileadapt":     "assistant-c0265496-86a2-46fd-a932-4c4398009013",
    "widget-stage1":  "assistant-9c42d3ce-e05a-4e34-8083-c91081917637",
}

# Default model and voice for new assistants
DEFAULT_MODEL = "google/gemini-2.5-flash"
DEFAULT_VOICE = "Telnyx.NaturalHD.eucalyptus"


# ── TelnyxClient ─────────────────────────────────────────────────────────────
class TelnyxClient:
    """
    SDK-first wrapper for Genesis Telnyx operations.
    All public methods return plain dicts (JSON-serialisable).
    """

    def __init__(self, api_key: Optional[str] = None) -> None:
        key = api_key or os.environ.get("TELNYX_API_KEY")
        if not key:
            raise RuntimeError(
                "TELNYX_API_KEY not found. Set env var or add to config/secrets.env"
            )
        self._client = Telnyx(api_key=key)

    # ── Resolve alias → full ID ───────────────────────────────────────────────
    def _resolve(self, id_or_alias: str) -> str:
        return KNOWN_ASSISTANTS.get(id_or_alias, id_or_alias)

    # ── Assistants ────────────────────────────────────────────────────────────

    def list_assistants(self) -> list[dict]:
        """Return all assistants as a list of dicts."""
        result = self._client.ai.assistants.list()
        assistants = getattr(result, "data", result)
        out = []
        for a in assistants:
            d = a if isinstance(a, dict) else vars(a)
            out.append({
                "id":          d.get("id", ""),
                "name":        d.get("name", ""),
                "model":       d.get("model", ""),
                "description": d.get("description", ""),
            })
        return out

    def get_assistant(self, assistant_id: str) -> dict:
        """Get full config of a single assistant."""
        assistant_id = self._resolve(assistant_id)
        result = self._client.ai.assistants.retrieve(assistant_id)
        return result if isinstance(result, dict) else vars(result)

    def create_assistant(
        self,
        name: str,
        instructions: str,
        model: str = DEFAULT_MODEL,
        voice: str = DEFAULT_VOICE,
        greeting: str = "",
        description: str = "",
        unauthenticated_web_calls: bool = True,
        noise_suppression: str = "krisp",
        time_limit_secs: int = 600,
        tools: Optional[list] = None,
        dvw_url: str = "",
    ) -> dict:
        """
        Create a new Telnyx AI assistant.

        Args:
            name:                      Display name
            instructions:             System prompt
            model:                    Model ID (default: google/gemini-2.5-flash)
            voice:                    TTS voice (default: Telnyx.NaturalHD.eucalyptus)
            greeting:                 Opening message (empty = user speaks first)
            description:              Internal description
            unauthenticated_web_calls: Allow unauthenticated browser widget calls
            noise_suppression:        "krisp" | "deepfilternet" | "disabled"
            time_limit_secs:          Max call duration
            tools:                    List of tool dicts (webhook tools etc.)
            dvw_url:                  Dynamic variables webhook URL

        Returns:
            dict with at least {"id": "assistant-xxxx", "name": ...}
        """
        kwargs: dict = dict(
            name=name,
            instructions=instructions,
            model=model,
            description=description,
            greeting=greeting,
            voice_settings={"voice": voice},
            telephony_settings={
                "supports_unauthenticated_web_calls": unauthenticated_web_calls,
                "noise_suppression": noise_suppression,
                "time_limit_secs": time_limit_secs,
            },
        )
        if tools:
            kwargs["tools"] = tools
        if dvw_url:
            kwargs["dynamic_variables_webhook_url"] = dvw_url

        result = self._client.ai.assistants.create(**kwargs)
        return result if isinstance(result, dict) else vars(result)

    def update_assistant(self, assistant_id: str, **fields) -> dict:
        """
        Update one or more fields on an existing assistant.

        Common fields:
            instructions, model, name, description, greeting,
            voice_settings, telephony_settings, tools, dvw_url,
            promote_to_main (bool)

        Example:
            client.update_assistant("aiva", instructions="New prompt",
                                    promote_to_main=True)
        """
        assistant_id = self._resolve(assistant_id)
        # Rename dvw_url → dynamic_variables_webhook_url
        if "dvw_url" in fields:
            fields["dynamic_variables_webhook_url"] = fields.pop("dvw_url")

        result = self._client.ai.assistants.update(assistant_id, **fields)
        return result if isinstance(result, dict) else vars(result)

    def update_instructions(
        self, assistant_id: str, instructions: str, promote: bool = True
    ) -> dict:
        """Update just the system prompt, optionally promoting to main."""
        return self.update_assistant(
            assistant_id,
            instructions=instructions,
            promote_to_main=promote,
        )

    def set_tools(self, assistant_id: str, tools: list, promote: bool = True) -> dict:
        """Replace all tools on an assistant."""
        return self.update_assistant(
            assistant_id, tools=tools, promote_to_main=promote
        )

    def add_webhook_tool(
        self,
        assistant_id: str,
        name: str,
        description: str,
        url: str,
        method: str = "POST",
        parameters: Optional[dict] = None,
    ) -> dict:
        """
        Add a single webhook tool to an assistant, preserving existing tools.

        Args:
            assistant_id: Assistant ID or alias
            name:         Tool function name (snake_case)
            description:  What the AI should use this tool for
            url:          Webhook endpoint URL
            method:       HTTP method (GET/POST)
            parameters:   JSON Schema for parameters (optional)

        Returns:
            Updated assistant dict
        """
        current = self.get_assistant(assistant_id)
        existing_tools = list(current.get("tools", []) or [])

        new_tool = {
            "type": "webhook",
            "name": name,
            "description": description,
            "webhook": {"url": url, "method": method},
        }
        if parameters:
            new_tool["parameters"] = parameters

        existing_tools.append(new_tool)
        return self.set_tools(assistant_id, existing_tools)

    # ── Versions ──────────────────────────────────────────────────────────────

    def list_versions(self, assistant_id: str) -> list[dict]:
        """List all versions of an assistant."""
        assistant_id = self._resolve(assistant_id)
        result = self._client.ai.assistants.versions.list(assistant_id)
        versions = getattr(result, "data", result)
        out = []
        for v in versions:
            d = v if isinstance(v, dict) else vars(v)
            out.append({
                "id":         d.get("id", ""),
                "version":    d.get("version", ""),
                "main":       d.get("main", False),
                "created_at": d.get("created_at", ""),
            })
        return out

    def promote_version(self, assistant_id: str, version_id: str) -> dict:
        """Promote a specific version to main (sends 100% of traffic to it)."""
        assistant_id = self._resolve(assistant_id)
        result = self._client.ai.assistants.versions.promote(
            version_id, assistant_id=assistant_id
        )
        return result if isinstance(result, dict) else vars(result)

    # ── Phone Numbers ─────────────────────────────────────────────────────────

    def search_numbers(
        self,
        country: str = "AU",
        area_code: Optional[str] = None,
        locality: Optional[str] = None,
        features: Optional[list[str]] = None,
        number_type: str = "local",
        limit: int = 10,
    ) -> list[dict]:
        """
        Search available phone numbers.

        Args:
            country:    ISO country code ("AU", "US", "GB")
            area_code:  National destination code / area code (e.g. "07" for QLD)
            locality:   City name (e.g. "Brisbane")
            features:   ["voice", "sms"] etc. Default: ["voice"]
            number_type: "local" | "toll_free" | "mobile"
            limit:       Max results

        Returns:
            List of {"phone_number", "region", "monthly_cost"}
        """
        filter_params: dict = {
            "country_code": country,
            "phone_number_type": number_type,
            "features": features or ["voice"],
            "limit": limit,
        }
        if area_code:
            filter_params["national_destination_code"] = area_code
        if locality:
            filter_params["locality"] = locality

        result = self._client.available_phone_numbers.list(filter=filter_params)
        numbers = getattr(result, "data", result)
        out = []
        for n in numbers:
            d = n if isinstance(n, dict) else vars(n)
            out.append({
                "phone_number":  d.get("phone_number", ""),
                "region":        d.get("region_information", [{}])[0].get("region_name", "") if d.get("region_information") else "",
                "monthly_cost":  d.get("cost_information", {}).get("monthly_cost", ""),
                "type":          d.get("phone_number_type", ""),
                "features":      d.get("features", []),
            })
        return out

    def list_my_numbers(self) -> list[dict]:
        """List all phone numbers in your Telnyx account."""
        result = self._client.phone_numbers.list()
        numbers = getattr(result, "data", result)
        out = []
        for n in numbers:
            d = n if isinstance(n, dict) else vars(n)
            out.append({
                "id":            d.get("id", ""),
                "phone_number":  d.get("phone_number", ""),
                "status":        d.get("status", ""),
                "connection_id": d.get("connection_id", ""),
                "tags":          d.get("tags", []),
            })
        return out

    def assign_number_to_connection(
        self, phone_number_id: str, connection_id: str
    ) -> dict:
        """
        Assign a phone number to a call-control connection.
        This routes inbound calls through the given connection.
        """
        result = self._client.phone_numbers.update(
            phone_number_id, connection_id=connection_id
        )
        return result if isinstance(result, dict) else vars(result)

    def tag_number(self, phone_number_id: str, tags: list[str]) -> dict:
        """Add tags to a phone number for organisation."""
        result = self._client.phone_numbers.update(phone_number_id, tags=tags)
        return result if isinstance(result, dict) else vars(result)

    # ── Audit ─────────────────────────────────────────────────────────────────

    def audit(self) -> dict:
        """
        Full system audit: all assistants + their tool counts,
        all owned phone numbers, known alias registry.

        Returns a structured dict suitable for logging or printing.
        """
        print("Auditing assistants...", end=" ", flush=True)
        assistants = self.list_assistants()
        print(f"{len(assistants)} found")

        print("Auditing phone numbers...", end=" ", flush=True)
        numbers = self.list_my_numbers()
        print(f"{len(numbers)} found")

        return {
            "assistants":       assistants,
            "phone_numbers":    numbers,
            "known_aliases":    KNOWN_ASSISTANTS,
            "default_model":    DEFAULT_MODEL,
            "default_voice":    DEFAULT_VOICE,
        }


# ── Convenience module-level functions (for import use) ──────────────────────

def get_client(api_key: Optional[str] = None) -> TelnyxClient:
    """Get a configured TelnyxClient instance."""
    return TelnyxClient(api_key=api_key)


def quick_update_instructions(assistant_alias: str, instructions: str) -> dict:
    """One-liner: update instructions on a known assistant."""
    return get_client().update_instructions(assistant_alias, instructions)


def quick_search_au_numbers(area_code: str = "07", limit: int = 5) -> list[dict]:
    """One-liner: find available QLD (07) phone numbers."""
    return get_client().search_numbers(country="AU", area_code=area_code, limit=limit)


# ── CLI ───────────────────────────────────────────────────────────────────────

def _print_json(data) -> None:
    print(json.dumps(data, indent=2, default=str))


def _cli_list(client: TelnyxClient, _args) -> None:
    assistants = client.list_assistants()
    print(f"\n{'ID':<50} {'NAME':<30} {'MODEL'}")
    print("-" * 100)
    for a in assistants:
        alias = next((k for k, v in KNOWN_ASSISTANTS.items() if v == a["id"]), "")
        alias_str = f" [{alias}]" if alias else ""
        print(f"{a['id']:<50} {(a['name'] + alias_str):<30} {a['model']}")
    print(f"\nTotal: {len(assistants)}")


def _cli_get(client: TelnyxClient, args) -> None:
    result = client.get_assistant(args.id)
    # Print key fields neatly
    for field in ["id", "name", "model", "description", "greeting"]:
        val = result.get(field, "")
        if val:
            print(f"{field:>15}: {val}")
    instructions = result.get("instructions", "")
    if instructions:
        print(f"\n{'instructions':>15}:\n{instructions[:500]}{'...' if len(instructions) > 500 else ''}")
    tools = result.get("tools", [])
    if tools:
        print(f"\n{'tools':>15}: {len(tools)} tool(s)")
        for t in tools:
            if isinstance(t, dict):
                t_name = t.get("name", t.get("type", "?"))
                webhook = t.get("webhook", {})
                t_url = webhook.get("url", "") if isinstance(webhook, dict) else ""
            else:
                # Pydantic model from SDK
                t_name = getattr(t, "name", getattr(t, "type", "?"))
                webhook = getattr(t, "webhook", None)
                t_url = getattr(webhook, "url", "") if webhook else ""
            print(f"{'':>17} - {t_name}: {t_url}")


def _cli_versions(client: TelnyxClient, args) -> None:
    versions = client.list_versions(args.id)
    print(f"\nVersions for {args.id}:")
    for v in versions:
        main_flag = " ← MAIN" if v.get("main") else ""
        print(f"  {v.get('id', ''):<50} {v.get('created_at', '')}{main_flag}")


def _cli_search(client: TelnyxClient, args) -> None:
    print(f"Searching {args.country} numbers (area: {args.area or 'any'}, limit: {args.limit})...")
    results = client.search_numbers(
        country=args.country,
        area_code=args.area or None,
        locality=args.locality or None,
        limit=args.limit,
    )
    if not results:
        print("No numbers found.")
        return
    print(f"\n{'PHONE NUMBER':<20} {'TYPE':<10} {'REGION':<25} {'MONTHLY'}")
    print("-" * 70)
    for n in results:
        print(f"{n['phone_number']:<20} {n['type']:<10} {n['region']:<25} {n['monthly_cost']}")


def _cli_numbers(client: TelnyxClient, _args) -> None:
    numbers = client.list_my_numbers()
    print(f"\n{'PHONE NUMBER':<20} {'ID':<45} {'STATUS':<15} {'CONNECTION'}")
    print("-" * 110)
    for n in numbers:
        print(f"{n['phone_number']:<20} {n['id']:<45} {n['status']:<15} {n.get('connection_id','')}")
    print(f"\nTotal: {len(numbers)}")


def _cli_audit(client: TelnyxClient, _args) -> None:
    result = client.audit()
    print("\n═══ GENESIS TELNYX AUDIT ═══\n")
    print(f"Assistants ({len(result['assistants'])}):")
    for a in result["assistants"]:
        alias = next((k for k, v in KNOWN_ASSISTANTS.items() if v == a["id"]), "")
        print(f"  [{alias:>14}] {a['id']}  {a['name']}")
    print(f"\nPhone numbers ({len(result['phone_numbers'])}):")
    for n in result["phone_numbers"]:
        print(f"  {n['phone_number']}  status={n['status']}  conn={n.get('connection_id','—')}")
    print(f"\nDefault model: {result['default_model']}")
    print(f"Default voice: {result['default_voice']}")


def _cli_promote(client: TelnyxClient, args) -> None:
    result = client.promote_version(args.id, args.version_id)
    print(f"Promoted version {args.version_id} to main for {args.id}")
    _print_json(result)


def _cli_assign(client: TelnyxClient, args) -> None:
    result = client.assign_number_to_connection(args.number_id, args.connection_id)
    print(f"Assigned {args.number_id} → connection {args.connection_id}")
    _print_json(result)


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Genesis Telnyx Client — SDK-first voice assistant manager",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    sub = parser.add_subparsers(dest="command", required=True)

    # list
    sub.add_parser("list", help="List all assistants")

    # get
    p_get = sub.add_parser("get", help="Get one assistant (full config)")
    p_get.add_argument("id", help="Assistant ID or alias (aiva, george, voice-terminal...)")

    # versions
    p_ver = sub.add_parser("versions", help="List versions of an assistant")
    p_ver.add_argument("id", help="Assistant ID or alias")

    # promote
    p_promo = sub.add_parser("promote", help="Promote a version to main")
    p_promo.add_argument("id", help="Assistant ID or alias")
    p_promo.add_argument("version_id", help="Version ID to promote")

    # numbers
    sub.add_parser("numbers", help="List your Telnyx phone numbers")

    # search
    p_search = sub.add_parser("search", help="Search available phone numbers")
    p_search.add_argument("--country", default="AU", help="Country code (default: AU)")
    p_search.add_argument("--area", default="", help="Area code / NDC (e.g. 07 for QLD)")
    p_search.add_argument("--locality", default="", help="City name")
    p_search.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")

    # audit
    sub.add_parser("audit", help="Full system audit")

    # assign
    p_assign = sub.add_parser("assign", help="Assign a phone number to a connection")
    p_assign.add_argument("number_id", help="Phone number ID")
    p_assign.add_argument("connection_id", help="Connection ID")

    args = parser.parse_args()

    client = TelnyxClient()

    dispatch = {
        "list":     _cli_list,
        "get":      _cli_get,
        "versions": _cli_versions,
        "promote":  _cli_promote,
        "numbers":  _cli_numbers,
        "search":   _cli_search,
        "audit":    _cli_audit,
        "assign":   _cli_assign,
    }

    handler = dispatch.get(args.command)
    if handler:
        handler(client, args)
    else:
        parser.print_help()


if __name__ == "__main__":
    main()
