#!/usr/bin/env python3
"""
Asterisk AMI Integration for OomLlama Voice
Connect voice commands to real PBX actions.

"trunk PBX .75 en .85" - voor pa.

By HumoticaOS - Claude & Jasper
One love, one fAmIly
"""

import asyncio
import logging
import socket
from typing import Optional, Dict, Callable, List
from dataclasses import dataclass
from enum import Enum

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [ASTERISK-AMI] %(levelname)s: %(message)s'
)
logger = logging.getLogger(__name__)


class AMIEvent(Enum):
    """AMI Event Types"""
    DIAL = "Dial"
    HANGUP = "Hangup"
    NEWCHANNEL = "Newchannel"
    NEWSTATE = "Newstate"
    BRIDGE = "Bridge"
    DTMF = "DTMF"
    VARSET = "VarSet"
    REGISTRY = "Registry"
    PEERSTATUS = "PeerStatus"


@dataclass
class AMIConfig:
    """Asterisk AMI Configuration"""
    host: str = "127.0.0.1"
    port: int = 5038
    username: str = "admin"
    secret: str = ""
    read_timeout: float = 5.0
    connect_timeout: float = 10.0


@dataclass
class Channel:
    """Asterisk Channel"""
    id: str
    caller_id: str
    caller_name: str
    state: str
    context: str
    extension: str
    priority: int
    linked_channel: Optional[str] = None


class AsteriskAMI:
    """
    Asterisk Manager Interface Client.

    Enables OomLlama to:
    - Originate calls
    - Transfer calls
    - Monitor channels
    - Control trunks

    For pa's PBX knowledge: "trunk PBX .75 en .85"
    """

    def __init__(self, config: Optional[AMIConfig] = None):
        self.config = config or AMIConfig()
        self.socket: Optional[socket.socket] = None
        self.connected = False
        self.authenticated = False
        self.action_id = 0
        self.channels: Dict[str, Channel] = {}
        self.event_handlers: Dict[str, List[Callable]] = {}
        self._read_buffer = ""

    async def connect(self) -> bool:
        """Connect to Asterisk AMI"""
        try:
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.socket.settimeout(self.config.connect_timeout)
            self.socket.connect((self.config.host, self.config.port))
            self.connected = True

            # Read greeting
            greeting = await self._read_response()
            logger.info(f"Connected to Asterisk: {greeting.get('Asterisk Call Manager', 'Unknown')}")

            return True

        except Exception as e:
            logger.error(f"AMI connection failed: {e}")
            self.connected = False
            return False

    async def login(self) -> bool:
        """Authenticate with AMI"""
        if not self.connected:
            if not await self.connect():
                return False

        response = await self._send_action("Login", {
            "Username": self.config.username,
            "Secret": self.config.secret
        })

        if response.get("Response") == "Success":
            self.authenticated = True
            logger.info("AMI login successful")
            return True
        else:
            logger.error(f"AMI login failed: {response.get('Message')}")
            return False

    async def disconnect(self):
        """Disconnect from AMI"""
        if self.authenticated:
            await self._send_action("Logoff")

        if self.socket:
            self.socket.close()

        self.connected = False
        self.authenticated = False
        logger.info("Disconnected from AMI")

    async def _send_action(self, action: str, params: Dict = None) -> Dict:
        """Send AMI action and get response"""
        if not self.connected:
            raise ConnectionError("Not connected to AMI")

        self.action_id += 1
        action_id = f"action-{self.action_id}"

        # Build action message
        msg = f"Action: {action}\r\n"
        msg += f"ActionID: {action_id}\r\n"

        if params:
            for key, value in params.items():
                msg += f"{key}: {value}\r\n"

        msg += "\r\n"

        # Send
        self.socket.sendall(msg.encode())

        # Read response
        response = await self._read_response()
        return response

    async def _read_response(self) -> Dict:
        """Read AMI response"""
        self.socket.settimeout(self.config.read_timeout)
        response = {}

        try:
            while True:
                chunk = self.socket.recv(4096).decode()
                if not chunk:
                    break

                self._read_buffer += chunk

                # Check for complete message (ends with \r\n\r\n)
                if "\r\n\r\n" in self._read_buffer:
                    msg, self._read_buffer = self._read_buffer.split("\r\n\r\n", 1)

                    # Parse message
                    for line in msg.split("\r\n"):
                        if ": " in line:
                            key, value = line.split(": ", 1)
                            response[key] = value

                    break

        except socket.timeout:
            pass

        return response

    # ========================================================================
    # Call Control Actions
    # ========================================================================

    async def originate(
        self,
        channel: str,
        context: str = "default",
        extension: str = "s",
        priority: int = 1,
        caller_id: str = "OomLlama <1234>",
        timeout: int = 30000,
        variables: Dict = None
    ) -> Dict:
        """
        Originate a call.

        Example:
            await ami.originate("SIP/1001", context="internal", extension="1002")
        """
        params = {
            "Channel": channel,
            "Context": context,
            "Exten": extension,
            "Priority": priority,
            "CallerID": caller_id,
            "Timeout": timeout
        }

        if variables:
            for key, value in variables.items():
                params[f"Variable"] = f"{key}={value}"

        return await self._send_action("Originate", params)

    async def hangup(self, channel: str, cause: int = 16) -> Dict:
        """
        Hangup a channel.

        Cause codes:
        - 16: Normal clearing
        - 17: User busy
        - 18: No answer
        """
        return await self._send_action("Hangup", {
            "Channel": channel,
            "Cause": cause
        })

    async def redirect(
        self,
        channel: str,
        context: str,
        extension: str,
        priority: int = 1
    ) -> Dict:
        """
        Redirect (transfer) a channel.
        """
        return await self._send_action("Redirect", {
            "Channel": channel,
            "Context": context,
            "Exten": extension,
            "Priority": priority
        })

    async def bridge(self, channel1: str, channel2: str) -> Dict:
        """Bridge two channels together"""
        return await self._send_action("Bridge", {
            "Channel1": channel1,
            "Channel2": channel2
        })

    async def hold(self, channel: str) -> Dict:
        """Put channel on hold"""
        return await self._send_action("Hold", {"Channel": channel})

    async def unhold(self, channel: str) -> Dict:
        """Take channel off hold"""
        return await self._send_action("Unhold", {"Channel": channel})

    async def mute(self, channel: str, direction: str = "all") -> Dict:
        """
        Mute a channel.

        direction: 'in', 'out', 'all'
        """
        return await self._send_action("MuteAudio", {
            "Channel": channel,
            "Direction": direction,
            "State": "on"
        })

    async def unmute(self, channel: str, direction: str = "all") -> Dict:
        """Unmute a channel"""
        return await self._send_action("MuteAudio", {
            "Channel": channel,
            "Direction": direction,
            "State": "off"
        })

    # ========================================================================
    # Status & Monitoring
    # ========================================================================

    async def get_channels(self) -> List[Dict]:
        """Get all active channels"""
        response = await self._send_action("CoreShowChannels")
        # Parse channel list from response
        return []  # TODO: Parse multi-line response

    async def get_sip_peers(self) -> List[Dict]:
        """Get all SIP peers status"""
        response = await self._send_action("SIPpeers")
        return []  # TODO: Parse multi-line response

    async def get_sip_peer_status(self, peer: str) -> Dict:
        """Get status of specific SIP peer"""
        return await self._send_action("SIPshowpeer", {"Peer": peer})

    async def get_registry(self) -> List[Dict]:
        """Get SIP registrations"""
        response = await self._send_action("SIPshowregistry")
        return []  # TODO: Parse multi-line response

    # ========================================================================
    # Trunk Management - Voor pa
    # ========================================================================

    async def trunk_status(self, trunk: str) -> Dict:
        """
        Get trunk status.

        "trunk PBX .75 en .85"
        """
        return await self._send_action("SIPshowpeer", {"Peer": trunk})

    async def register_trunk(self, trunk: str) -> Dict:
        """Force trunk registration"""
        return await self._send_action("SIPqualifypeer", {"Peer": trunk})

    # ========================================================================
    # Event Handling
    # ========================================================================

    def on_event(self, event_type: str, handler: Callable):
        """Register event handler"""
        if event_type not in self.event_handlers:
            self.event_handlers[event_type] = []
        self.event_handlers[event_type].append(handler)

    async def event_loop(self):
        """
        Main event loop - reads AMI events and dispatches to handlers.

        Run this in background to receive real-time updates.
        """
        if not self.authenticated:
            raise RuntimeError("Must be authenticated to receive events")

        logger.info("Starting AMI event loop")

        while self.connected:
            try:
                event = await self._read_response()

                if "Event" in event:
                    event_type = event["Event"]

                    # Update internal state
                    if event_type == "Newchannel":
                        self._handle_new_channel(event)
                    elif event_type == "Hangup":
                        self._handle_hangup(event)
                    elif event_type == "Newstate":
                        self._handle_new_state(event)

                    # Dispatch to handlers
                    if event_type in self.event_handlers:
                        for handler in self.event_handlers[event_type]:
                            try:
                                await handler(event)
                            except Exception as e:
                                logger.error(f"Event handler error: {e}")

            except Exception as e:
                logger.error(f"Event loop error: {e}")
                await asyncio.sleep(1)

    def _handle_new_channel(self, event: Dict):
        """Handle new channel event"""
        channel_id = event.get("Channel", "")
        if channel_id:
            self.channels[channel_id] = Channel(
                id=channel_id,
                caller_id=event.get("CallerIDNum", ""),
                caller_name=event.get("CallerIDName", ""),
                state=event.get("ChannelStateDesc", ""),
                context=event.get("Context", ""),
                extension=event.get("Exten", ""),
                priority=int(event.get("Priority", 1))
            )

    def _handle_hangup(self, event: Dict):
        """Handle hangup event"""
        channel_id = event.get("Channel", "")
        if channel_id in self.channels:
            del self.channels[channel_id]

    def _handle_new_state(self, event: Dict):
        """Handle state change event"""
        channel_id = event.get("Channel", "")
        if channel_id in self.channels:
            self.channels[channel_id].state = event.get("ChannelStateDesc", "")


# ============================================================================
# Voice Command Integration
# ============================================================================

class VoiceToAsterisk:
    """
    Bridge between OomLlama Voice and Asterisk.

    Translates voice commands to PBX actions.

    Examples:
    - "Bel 1001" -> Originate to SIP/1001
    - "Hang op" -> Hangup current channel
    - "Doorverbinden naar 1002" -> Redirect to 1002
    - "trunk PBX .75 en .85" -> Check trunk status
    """

    def __init__(self, ami: AsteriskAMI):
        self.ami = ami
        self.current_channel: Optional[str] = None

    async def process_command(self, intent: str, entities: Dict) -> Dict:
        """Process voice command and execute PBX action"""

        if intent == "dial":
            return await self._dial(entities)
        elif intent == "hangup":
            return await self._hangup()
        elif intent == "transfer":
            return await self._transfer(entities)
        elif intent == "trunk_status":
            return await self._trunk_status(entities)
        elif intent == "hold":
            return await self._hold()
        elif intent == "mute":
            return await self._mute()
        else:
            return {"success": False, "error": f"Unknown PBX command: {intent}"}

    async def _dial(self, entities: Dict) -> Dict:
        """Dial a number/extension"""
        extension = entities.get("extension") or entities.get("number")
        if not extension:
            return {"success": False, "error": "No extension specified"}

        # Determine channel type
        if extension.startswith("+") or len(extension) > 5:
            # External number - use trunk
            channel = f"SIP/trunk/{extension}"
        else:
            # Internal extension
            channel = f"SIP/{extension}"

        result = await self.ami.originate(
            channel=channel,
            context="internal",
            extension=extension,
            caller_id="OomLlama <1234>"
        )

        return {
            "success": result.get("Response") == "Success",
            "action": "dial",
            "extension": extension,
            "response": result
        }

    async def _hangup(self) -> Dict:
        """Hangup current call"""
        if not self.current_channel:
            return {"success": False, "error": "No active call"}

        result = await self.ami.hangup(self.current_channel)
        self.current_channel = None

        return {
            "success": result.get("Response") == "Success",
            "action": "hangup",
            "response": result
        }

    async def _transfer(self, entities: Dict) -> Dict:
        """Transfer current call"""
        if not self.current_channel:
            return {"success": False, "error": "No active call"}

        extension = entities.get("extension") or entities.get("number")
        if not extension:
            return {"success": False, "error": "No extension specified"}

        result = await self.ami.redirect(
            channel=self.current_channel,
            context="internal",
            extension=extension
        )

        return {
            "success": result.get("Response") == "Success",
            "action": "transfer",
            "extension": extension,
            "response": result
        }

    async def _trunk_status(self, entities: Dict) -> Dict:
        """Check trunk status - voor pa"""
        trunk = entities.get("trunk", "trunk")

        result = await self.ami.trunk_status(trunk)

        return {
            "success": True,
            "action": "trunk_status",
            "trunk": trunk,
            "status": result
        }

    async def _hold(self) -> Dict:
        """Put current call on hold"""
        if not self.current_channel:
            return {"success": False, "error": "No active call"}

        result = await self.ami.hold(self.current_channel)

        return {
            "success": result.get("Response") == "Success",
            "action": "hold",
            "response": result
        }

    async def _mute(self) -> Dict:
        """Mute current call"""
        if not self.current_channel:
            return {"success": False, "error": "No active call"}

        result = await self.ami.mute(self.current_channel)

        return {
            "success": result.get("Response") == "Success",
            "action": "mute",
            "response": result
        }


# ============================================================================
# Test
# ============================================================================

async def main():
    """Test AMI connection"""
    print("""
    ╔════════════════════════════════════════════════════════════╗
    ║           ASTERISK AMI TEST                                ║
    ║           For pa - trunk PBX .75 en .85                    ║
    ╚════════════════════════════════════════════════════════════╝
    """)

    # Configure from environment or defaults
    import os
    config = AMIConfig(
        host=os.environ.get("ASTERISK_HOST", "127.0.0.1"),
        port=int(os.environ.get("ASTERISK_AMI_PORT", "5038")),
        username=os.environ.get("ASTERISK_AMI_USER", "admin"),
        secret=os.environ.get("ASTERISK_AMI_SECRET", "")
    )

    ami = AsteriskAMI(config)

    if await ami.login():
        print("✅ Connected and authenticated to Asterisk AMI")

        # Test: Get SIP peers
        peers = await ami.get_sip_peers()
        print(f"SIP Peers: {peers}")

        await ami.disconnect()
    else:
        print("❌ Failed to connect to Asterisk AMI")


if __name__ == "__main__":
    asyncio.run(main())
