"""
pygwan — Unofficial Python wrapper for the WhatsApp Cloud API
By Tarmica Chiwara

v3: Added TemplateComponents builder, flow/simple template helpers,
    and human_reply for natural bot interactions.
"""
from __future__ import annotations

import logging
import mimetypes
import os
import time
import warnings
from typing import Any, Dict, List, Optional, Union

import requests
from colorama import Fore, Style

logging.getLogger(__name__).addHandler(logging.NullHandler())


# ---------------------------------------------------------------------------
# TemplateComponents — fluent builder so developers never touch raw dicts
# ---------------------------------------------------------------------------

class TemplateComponents:
    """
    Fluent builder for WhatsApp template message components.

    Eliminates the need to manually construct component dicts.
    Chain methods and call .build() to get the final list.

    Example — body-only template:
        components = TemplateComponents().add_body("John", "Order #1023").build()

    Example — template with header, body, and a flow button:
        components = (
            TemplateComponents()
            .add_header("Welcome!")
            .add_body("Hi John", "your account is ready")
            .add_flow_button()
            .build()
        )

    Example — template with a quick reply button:
        components = (
            TemplateComponents()
            .add_body("Confirm your order?")
            .add_quick_reply_button("CONFIRM_YES")
            .build()
        )
    """

    def __init__(self):
        self._components: List[Dict] = []
        self._button_index: int = 0

    def add_header(self, *variables: str) -> "TemplateComponents":
        """
        Adds a header component with text variables.

        Args:
            *variables: One or more string values to fill {{1}}, {{2}}... placeholders.
        """
        self._components.append({
            "type": "header",
            "parameters": [{"type": "text", "text": str(v)} for v in variables]
        })
        return self

    def add_body(self, *variables: str) -> "TemplateComponents":
        """
        Adds a body component with text variables.

        Args:
            *variables: One or more string values to fill {{1}}, {{2}}... placeholders.
        """
        self._components.append({
            "type": "body",
            "parameters": [{"type": "text", "text": str(v)} for v in variables]
        })
        return self

    def add_flow_button(
        self, flow_token: str = "unused", index: Optional[int] = None
    ) -> "TemplateComponents":
        """
        Adds a Flow button component.

        Required for any template that contains a WhatsApp Flow. Handles
        the sub_type and action structure automatically.

        Args:
            flow_token: Token passed into the flow for session tracking.
                        Use "unused" if you don't need session tracking.
            index: Button index. Auto-increments if not provided.
        """
        idx = str(index if index is not None else self._button_index)
        self._components.append({
            "type": "button",
            "sub_type": "flow",
            "index": idx,
            "parameters": [
                {
                    "type": "action",
                    "action": {"flow_token": flow_token}
                }
            ]
        })
        self._button_index += 1
        return self

    def add_quick_reply_button(
        self, payload: str, index: Optional[int] = None
    ) -> "TemplateComponents":
        """
        Adds a quick reply button component.

        Args:
            payload: The payload string returned when the button is tapped.
            index: Button index. Auto-increments if not provided.
        """
        idx = str(index if index is not None else self._button_index)
        self._components.append({
            "type": "button",
            "sub_type": "quick_reply",
            "index": idx,
            "parameters": [{"type": "payload", "payload": payload}]
        })
        self._button_index += 1
        return self

    def add_url_button(
        self, url_suffix: str, index: Optional[int] = None
    ) -> "TemplateComponents":
        """
        Adds a dynamic URL button component.

        Args:
            url_suffix: The dynamic part appended to the template's base URL.
            index: Button index. Auto-increments if not provided.
        """
        idx = str(index if index is not None else self._button_index)
        self._components.append({
            "type": "button",
            "sub_type": "url",
            "index": idx,
            "parameters": [{"type": "text", "text": url_suffix}]
        })
        self._button_index += 1
        return self

    def build(self) -> List[Dict]:
        """Returns the final list of component dicts, ready for send_template."""
        return self._components


# ---------------------------------------------------------------------------
# WhatsApp — main client
# ---------------------------------------------------------------------------

class WhatsApp(object):
    """
    WhatsApp Cloud API client.

    Args:
        token (str): Bearer token from the Meta developer portal.
        phone_number_id (str): Phone number ID from the Meta developer portal.
    """

    def __init__(self, token: str = None, phone_number_id: str = None):
        self.token = token
        self.phone_number_id = phone_number_id
        self.base_url = "https://graph.facebook.com/v24.0"
        self.v15_base_url = "https://graph.facebook.com/v24.0"
        self.url = f"{self.base_url}/{phone_number_id}/messages"
        self.headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.token}",
        }

    # -----------------------------------------------------------------------
    # Messaging — text
    # -----------------------------------------------------------------------

    def send_message(
        self,
        message: str,
        recipient_id: str,
        recipient_type: str = "individual",
        preview_url: bool = True,
    ) -> Dict:
        """
        Sends a text message. Automatically splits messages over 4096 characters.

        Args:
            message: Text to send.
            recipient_id: Recipient phone number with country code, no +.
            recipient_type: "individual" or "group".
            preview_url: Whether to render link previews.

        Example:
            >>> wa.send_message("Hello!", "263779281345")
        """
        def _send(msg):
            data = {
                "messaging_product": "whatsapp",
                "recipient_type": recipient_type,
                "to": recipient_id,
                "type": "text",
                "text": {"preview_url": preview_url, "body": msg},
            }
            logging.info(f"Sending message to {recipient_id}")
            r = requests.post(self.url, headers=self.headers, json=data)
            if r.status_code == 200:
                logging.info(f"Message sent to {recipient_id}")
            else:
                logging.error(f"Message not sent to {recipient_id} — {r.status_code}: {r.json()}")
            return r.json()

        if len(message) > 4096:
            chunks = [message[i:i + 4096] for i in range(0, len(message), 4096)]
            for chunk in chunks:
                result = _send(chunk)
            return result
        return _send(message)

    def reply_to_message(
        self,
        message_id: str,
        recipient_id: str,
        message: str,
        preview_url: bool = True,
    ) -> Dict:
        """
        Replies to a specific message by ID (shows quoted message in chat).

        Args:
            message_id: The ID of the message to reply to.
            recipient_id: Recipient phone number with country code, no +.
            message: Reply text.
            preview_url: Whether to render link previews.
        """
        def _send(msg):
            data = {
                "messaging_product": "whatsapp",
                "recipient_type": "individual",
                "to": recipient_id,
                "type": "text",
                "context": {"message_id": message_id},
                "text": {"preview_url": preview_url, "body": msg},
            }
            logging.info(f"Replying to {message_id}")
            r = requests.post(self.url, headers=self.headers, json=data)
            if r.status_code == 200:
                logging.info(f"Reply sent to {recipient_id}")
            else:
                logging.error(f"Reply not sent — {r.status_code}: {r.json()}")
            return r.json()

        if len(message) > 4096:
            chunks = [message[i:i + 4096] for i in range(0, len(message), 4096)]
            for chunk in chunks:
                result = _send(chunk)
            return result
        return _send(message)

    def send_reaction(
        self, message_id: str, reaction: str, recipient_id: str
    ) -> Dict:
        """
        Reacts to a message with an emoji.

        Args:
            message_id: ID of the message to react to.
            reaction: Unicode emoji (e.g. "\\U0001F44D" for 👍).
            recipient_id: Phone number with country code, no +.
        """
        data = {
            "messaging_product": "whatsapp",
            "recipient_type": "individual",
            "to": recipient_id,
            "type": "reaction",
            "reaction": {"message_id": message_id, "emoji": reaction},
        }
        logging.info(f"Sending reaction to {recipient_id}")
        r = requests.post(self.url, headers=self.headers, json=data)
        if r.status_code == 200:
            logging.info(f"Reaction sent to {recipient_id}")
        else:
            logging.error(f"Reaction not sent — {r.status_code}: {r.json()}")
        return r.json()

    # -----------------------------------------------------------------------
    # The Rabbit 🐇 — human_reply
    # -----------------------------------------------------------------------

    def human_reply(
        self,
        message_id: str,
        recipient_id: str,
        message: str,
        delay: float = 1.5,
        preview_url: bool = True,
    ) -> Dict:
        """
        ✨ Makes your bot feel human.

        Chains three steps automatically:
            1. Marks the incoming message as read (shows blue ticks).
            2. Shows the typing indicator bubble (... in chat).
            3. Waits `delay` seconds (simulates thinking/typing time).
            4. Sends the reply.

        This single method transforms a robotic instant-response into a
        natural conversation. Tune `delay` based on message length for
        even more realism.

        Args:
            message_id: ID of the message being replied to.
            recipient_id: Phone number with country code, no +.
            message: The reply text to send.
            delay: Seconds to wait before sending. Default 1.5.
                   Tip: use len(message) * 0.03 for length-proportional delay.
            preview_url: Whether to render link previews.

        Example — basic:
            >>> wa.human_reply(message_id, "263779281345", "Sure, let me check that for you!")

        Example — length-aware delay (feels like real typing speed):
            >>> wa.human_reply(
            ...     message_id, "263779281345",
            ...     "Here is your full invoice breakdown...",
            ...     delay=len(response_text) * 0.03
            ... )
        """
        # Step 1 & 2: read receipt + typing bubble
        self.mark_as_read(message_id, typing=True)
        # Step 3: simulate thinking/typing
        time.sleep(delay)
        # Step 4: send
        return self.reply_to_message(message_id, recipient_id, message, preview_url)

    # -----------------------------------------------------------------------
    # Templates
    # -----------------------------------------------------------------------

    def send_template(
        self,
        template: str,
        recipient_id: str,
        components: List[Dict],
        lang: str = "en_US",
    ) -> Dict:
        """
        Sends a template message with full component control.

        For common cases, prefer the higher-level helpers:
            - send_simple_template   — no variables, no buttons
            - send_flow_template     — template containing a Flow button
            - send_payload_template  — body variables only
            - send_payload_template_with_header — header + body variables

        For full control, build components with TemplateComponents:
            >>> from pygwan import WhatsApp, TemplateComponents
            >>> components = (
            ...     TemplateComponents()
            ...     .add_body("Alice", "Order #99")
            ...     .add_flow_button()
            ...     .build()
            ... )
            >>> wa.send_template("my_template", "263779281345", components)

        Args:
            template: Template name as registered in Meta Business dashboard.
            recipient_id: Phone number with country code, no +.
            components: List of component dicts. Use TemplateComponents.build().
            lang: BCP-47 language code matching your template's language.
        """
        data = {
            "messaging_product": "whatsapp",
            "to": recipient_id,
            "type": "template",
            "template": {
                "name": template,
                "language": {"code": lang},
                "components": components,
            },
        }
        logging.info(f"Sending template '{template}' to {recipient_id}")
        r = requests.post(self.url, headers=self.headers, json=data)
        if r.status_code == 200:
            logging.info(f"Template sent to {recipient_id}")
        else:
            logging.error(f"Template not sent — {r.status_code}: {r.json()}")
        return r.json()

    def send_simple_template(
        self, template: str, recipient_id: str, lang: str = "en_US"
    ) -> Dict:
        """
        Sends a template that has no dynamic variables and no special buttons.

        Use this for templates like "hello_world" that are purely static text.

        Args:
            template: Template name.
            recipient_id: Phone number with country code, no +.
            lang: Language code matching the template.

        Example:
            >>> wa.send_simple_template("hello_world", "263779281345")
        """
        return self.send_template(template, recipient_id, components=[], lang=lang)

    def send_flow_template(
        self,
        template: str,
        recipient_id: str,
        flow_token: str = "unused",
        body_variables: Optional[List[str]] = None,
        header_variables: Optional[List[str]] = None,
        lang: str = "en_US",
    ) -> Dict:
        """
        Sends a template that contains a WhatsApp Flow button.

        Handles the flow component structure automatically — you never need
        to know about sub_type, flow_token action format, or button indexing.

        Args:
            template: Template name.
            recipient_id: Phone number with country code, no +.
            flow_token: Passed into the flow for session tracking.
                        Use "unused" if you don't need to track sessions.
            body_variables: Optional list of strings for body {{placeholders}}.
            header_variables: Optional list of strings for header {{placeholders}}.
            lang: Language code matching the template.

        Example — no variables:
            >>> wa.send_flow_template("agentd", "263779281345")

        Example — with body variables:
            >>> wa.send_flow_template(
            ...     "onboarding_flow", "263779281345",
            ...     body_variables=["Alice"],
            ...     flow_token="session-abc123"
            ... )
        """
        components = TemplateComponents()

        if header_variables:
            components.add_header(*header_variables)

        if body_variables:
            components.add_body(*body_variables)

        components.add_flow_button(flow_token=flow_token)

        return self.send_template(template, recipient_id, components.build(), lang=lang)

    def send_payload_template(
        self,
        template_name: str,
        recipient_id: str,
        variables: List[str],
        lang: str = "en_US",
    ) -> requests.Response:
        """
        Sends a template with body-only dynamic variables.

        Args:
            template_name: Template name.
            recipient_id: Phone number with country code, no +.
            variables: List of strings filling body {{placeholders}} in order.
            lang: Language code.

        Example:
            >>> wa.send_payload_template("invoice_ready", "263779281345", ["Alice", "#1023"])
        """
        components = TemplateComponents().add_body(*variables).build()
        data = {
            "messaging_product": "whatsapp",
            "to": recipient_id,
            "type": "template",
            "template": {
                "name": template_name,
                "language": {"code": lang},
                "components": components,
            },
        }
        response = requests.post(self.url, headers=self.headers, json=data)
        if response.status_code == 200:
            logging.info(f"Template message sent to {recipient_id}")
        else:
            logging.error(f"Failed to send template: {response.json()}")
        return response

    def send_payload_template_with_header(
        self,
        template_name: str,
        recipient_id: str,
        header_variables: List[str],
        body_variables: List[str],
        lang: str = "en_US",
    ) -> requests.Response:
        """
        Sends a template with both header and body dynamic variables.

        Args:
            template_name: Template name.
            recipient_id: Phone number with country code, no +.
            header_variables: Variables for header {{placeholders}}.
            body_variables: Variables for body {{placeholders}}.
            lang: Language code.

        Example:
            >>> wa.send_payload_template_with_header(
            ...     "order_update", "263779281345",
            ...     header_variables=["Invoice #1023"],
            ...     body_variables=["Alice", "shipped"]
            ... )
        """
        components = (
            TemplateComponents()
            .add_header(*header_variables)
            .add_body(*body_variables)
            .build()
        )
        data = {
            "messaging_product": "whatsapp",
            "to": recipient_id,
            "type": "template",
            "template": {
                "name": template_name,
                "language": {"code": lang},
                "components": components,
            },
        }
        response = requests.post(self.url, headers=self.headers, json=data)
        if response.status_code == 200:
            logging.info(f"Template message sent to {recipient_id}")
        else:
            logging.error(f"Failed to send template: {response.json()}")
        return response

    def send_templatev2(
        self, template, recipient_id, components, lang: str = "en_US"
    ):
        message = (
            f"{Fore.RED}send_templatev2 is deprecated. "
            f"Use send_template instead.{Style.RESET_ALL}"
        )
        warnings.warn(message, DeprecationWarning)
        return self.send_template(template, recipient_id, components, lang=lang)

    # -----------------------------------------------------------------------
    # Media
    # -----------------------------------------------------------------------

    def send_image(
        self,
        image: str,
        recipient_id: str,
        recipient_type: str = "individual",
        caption: Optional[str] = None,
        link: bool = True,
    ) -> Dict:
        """
        Sends an image message.

        Args:
            image: Image URL (if link=True) or media ID (if link=False).
            recipient_id: Phone number with country code, no +.
            recipient_type: "individual" or "group".
            caption: Optional image caption.
            link: True = image is a URL, False = image is a media ID.

        Example:
            >>> wa.send_image("https://example.com/photo.jpg", "263779281345")
        """
        image_data = {"link": image} if link else {"id": image}
        if caption:
            image_data["caption"] = caption
        data = {
            "messaging_product": "whatsapp",
            "recipient_type": recipient_type,
            "to": recipient_id,
            "type": "image",
            "image": image_data,
        }
        logging.info(f"Sending image to {recipient_id}")
        r = requests.post(self.url, headers=self.headers, json=data)
        if r.status_code == 200:
            logging.info(f"Image sent to {recipient_id}")
        else:
            logging.error(f"Image not sent — {r.status_code}: {r.json()}")
        return r.json()

    def send_document(
        self,
        document_url: str,
        recipient_id: str,
        caption: str = "",
        filename: str = "",
    ) -> Dict:
        """
        Sends a document (PDF, DOCX, etc.) by URL.

        Args:
            document_url: Publicly accessible URL of the document.
            recipient_id: Phone number with country code, no +.
            caption: Optional caption shown below the document.
            filename: Display filename including extension (e.g. "invoice.pdf").
        """
        data = {
            "messaging_product": "whatsapp",
            "recipient_type": "individual",
            "to": recipient_id,
            "type": "document",
            "document": {
                "link": document_url,
                "caption": caption,
                "filename": filename,
            },
        }
        response = requests.post(self.url, headers=self.headers, json=data)
        if response.status_code == 200:
            logging.info(f"Document sent to {recipient_id}")
        else:
            logging.error(f"Document not sent — {response.text}")
        return response.json()

    def upload_media(self, media_path: str, category: str) -> str:
        """
        Uploads a local media file to the WhatsApp Cloud API.

        Args:
            media_path: Absolute or relative path to the file.
            category: One of "image", "video", "audio", "document".

        Returns:
            The media ID string assigned by the API.

        Raises:
            FileNotFoundError: If the file does not exist.
            ValueError: If the category or extension is unsupported.

        Example:
            >>> media_id = wa.upload_media("/tmp/photo.png", "image")
        """
        MEDIA_CATEGORIES = {
            "image":    {"extensions": [".jpeg", ".jpg", ".png", ".webp"],           "default_mime": "image/png"},
            "video":    {"extensions": [".mp4", ".3gp"],                             "default_mime": "video/mp4"},
            "audio":    {"extensions": [".aac", ".amr", ".mp3", ".m4a", ".ogg"],    "default_mime": "audio/mpeg"},
            "document": {"extensions": [".txt", ".pdf", ".doc", ".docx", ".xls",
                                        ".xlsx", ".ppt", ".pptx"],                  "default_mime": "application/octet-stream"},
        }
        if not os.path.isfile(media_path):
            raise FileNotFoundError(f"Media file not found: {media_path}")

        category = category.lower()
        if category not in MEDIA_CATEGORIES:
            raise ValueError(f"Unsupported category '{category}'. Choose from {list(MEDIA_CATEGORIES)}.")

        ext = os.path.splitext(media_path)[1].lower()
        cat = MEDIA_CATEGORIES[category]
        if ext not in cat["extensions"]:
            raise ValueError(
                f"Extension '{ext}' not supported for '{category}'. Supported: {cat['extensions']}"
            )

        mime_type, _ = mimetypes.guess_type(media_path)
        if not mime_type:
            mime_type = cat["default_mime"]

        url = f"{self.base_url}/{self.phone_number_id}/media"
        headers = {"Authorization": f"Bearer {self.token}"}
        f = open(media_path, "rb")
        files = {"file": (os.path.basename(media_path), f, mime_type)}
        data = {"messaging_product": "whatsapp"}

        response = requests.post(url, headers=headers, files=files, data=data)
        f.close()

        result = response.json()
        if response.status_code != 200:
            logging.error(f"Upload failed ({response.status_code}): {result.get('error', {}).get('message', response.text)}")

        return result.get("id") or result.get("media", [{}])[0].get("id")

    def download_media(
        self, media_url: str, mime_type: str, file_path: str = "temp"
    ) -> Optional[str]:
        """
        Downloads media from a WhatsApp media URL.

        Args:
            media_url: The media URL (from webhook or upload response).
            mime_type: MIME type, e.g. "image/jpeg", "video/mp4".
            file_path: Save path without extension. Default is "temp".

        Returns:
            The saved file path, or None on failure.
        """
        r = requests.get(media_url, headers=self.headers)
        extension = mime_type.split("/")[1]
        save_path = f"{file_path}.{extension}"
        try:
            with open(save_path, "wb") as f:
                f.write(r.content)
            logging.info(f"Media downloaded to {save_path}")
            return save_path
        except Exception as e:
            logging.error(f"Error downloading media: {e}")
            return None

    # -----------------------------------------------------------------------
    # Interactive messages
    # -----------------------------------------------------------------------

    def mark_as_read(self, message_id: str, **kwargs) -> Dict:
        """
        Marks a message as read and optionally shows a typing indicator.

        Args:
            message_id: ID of the message to mark as read.
            typing (bool): If True, show the typing bubble alongside the read receipt.

        Example:
            >>> wa.mark_as_read(message_id)
            >>> wa.mark_as_read(message_id, typing=True)
        """
        headers = {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json",
        }
        json_data: Dict[str, Any] = {
            "messaging_product": "whatsapp",
            "status": "read",
            "message_id": message_id,
        }
        if kwargs.get("typing"):
            json_data["typing_indicator"] = {"type": "text"}

        logging.info(f"Marking message {message_id} as read")
        response = requests.post(
            f"{self.v15_base_url}/{self.phone_number_id}/messages",
            headers=headers,
            json=json_data,
        ).json()
        return response

    def send_button(self, button: Dict[Any, Any], recipient_id: str) -> Dict:
        """
        Sends an interactive list button message.

        Args:
            button: Dict with keys: header, body, footer, action.
            recipient_id: Phone number with country code, no +.

        Example:
            >>> wa.send_button({
            ...     "header": "Choose a package",
            ...     "body": "Select from below",
            ...     "footer": "Powered by pygwan",
            ...     "action": {"buttons": [{"id": "1", "title": "Basic"}]}
            ... }, "263779281345")
        """
        data = {
            "messaging_product": "whatsapp",
            "to": recipient_id,
            "type": "interactive",
            "interactive": self._build_list_interactive(button),
        }
        r = requests.post(self.url, headers=self.headers, json=data)
        if r.status_code == 200:
            logging.info(f"Buttons sent to {recipient_id}")
        else:
            logging.error(f"Buttons not sent — {r.status_code}: {r.json()}")
        return r.json()

    def send_reply_button(self, button: Dict[Any, Any], recipient_id: str) -> Dict:
        """
        Sends an interactive reply button message (max 3 buttons).

        Args:
            button: Full interactive payload dict.
            recipient_id: Phone number with country code, no +.
        """
        data = {
            "messaging_product": "whatsapp",
            "recipient_type": "individual",
            "to": recipient_id,
            "type": "interactive",
            "interactive": button,
        }
        r = requests.post(self.url, headers=self.headers, json=data)
        if r.status_code == 200:
            logging.info(f"Reply buttons sent to {recipient_id}")
        else:
            logging.error(f"Reply buttons not sent — {r.status_code}: {r.json()}")
        return r.json()

    def send_list_message(
        self,
        recipient_id: str,
        header: str,
        body: str,
        footer: str,
        buttons: List[Dict],
    ) -> Dict:
        """
        Sends an interactive list message.

        Args:
            recipient_id: Phone number with country code, no +.
            header: Header text.
            body: Body text.
            footer: Footer text.
            buttons: List of dicts with keys "title" and "payload".

        Example:
            >>> wa.send_list_message(
            ...     "263779281345", "Choose", "Select below", "Footer",
            ...     [{"title": "Option A", "payload": "A"}, {"title": "Option B", "payload": "B"}]
            ... )
        """
        sections = [{
            "title": "Select one",
            "rows": [{"id": b["payload"], "title": b["title"]} for b in buttons]
        }]
        data = {
            "messaging_product": "whatsapp",
            "to": recipient_id,
            "type": "interactive",
            "interactive": {
                "type": "list",
                "header": {"type": "text", "text": header},
                "body": {"text": body},
                "footer": {"text": footer},
                "action": {"button": "Choose", "sections": sections},
            },
        }
        response = requests.post(self.url, headers=self.headers, json=data)
        if response.status_code == 200:
            logging.info(f"List message sent to {recipient_id}")
        else:
            logging.error(f"List message not sent — {response.text}")
        return response.json()

    def send_cta_url_button(
        self,
        recipient_id: str,
        button_text: str,
        url: str,
        message: Optional[str] = None,
    ) -> Dict:
        """
        Sends a call-to-action URL button.

        Args:
            recipient_id: Phone number with country code, no +.
            button_text: Label displayed on the button.
            url: URL opened when the button is tapped.
            message: Optional body text shown above the button.
        """
        body = {"body": {"text": message}} if message else {}
        data = {
            "messaging_product": "whatsapp",
            "recipient_type": "individual",
            "to": recipient_id,
            "type": "interactive",
            "interactive": {
                "type": "cta_url",
                **body,
                "action": {
                    "name": "cta_url",
                    "parameters": {"display_text": button_text, "url": url},
                },
            },
        }
        response = requests.post(self.url, headers=self.headers, json=data)
        if response.status_code == 200:
            logging.info(f"CTA URL button sent to {recipient_id}")
        else:
            logging.error(f"CTA URL button not sent — {response.text}")
        return response.json()

    def request_location(self, recipient_id: str, message: str) -> Dict:
        """
        Sends a location request to the user.

        Args:
            recipient_id: Phone number with country code, no +.
            message: Prompt shown to the user (max 1024 characters).
        """
        if len(message) >= 1025:
            raise ValueError("Message length must not exceed 1024 characters.")
        data = {
            "messaging_product": "whatsapp",
            "recipient_type": "individual",
            "type": "interactive",
            "to": recipient_id,
            "interactive": {
                "type": "location_request_message",
                "body": {"text": message},
                "action": {"name": "send_location"},
            },
        }
        response = requests.post(self.url, headers=self.headers, json=data)
        if response.status_code == 200:
            logging.info(f"Location request sent to {recipient_id}")
        else:
            logging.error(f"Location request not sent — {response.text}")
        return response.json()

    # -----------------------------------------------------------------------
    # Webhook parsing helpers
    # -----------------------------------------------------------------------

    def preprocess(self, data: Dict[Any, Any]) -> Dict[Any, Any]:
        """Extracts the value payload from raw webhook data."""
        return data["entry"][0]["changes"][0]["value"]

    def is_message(self, data: Dict[Any, Any]) -> bool:
        """Returns True if the webhook event contains an inbound message."""
        return "messages" in self.preprocess(data)

    def get_mobile(self, data: Dict[Any, Any]) -> Optional[str]:
        """Returns the sender's WhatsApp ID (phone number)."""
        processed = self.preprocess(data)
        if "contacts" in processed:
            return processed["contacts"][0]["wa_id"]
        return None

    def get_name(self, data: Dict[Any, Any]) -> Optional[str]:
        """Returns the sender's display name."""
        processed = self.preprocess(data)
        if processed:
            return processed["contacts"][0]["profile"]["name"]
        return None

    def get_message(self, data: Dict[Any, Any]) -> Optional[str]:
        """
        Returns the message text or interactive button title.
        Handles text messages and button_reply interactive types.
        """
        processed = self.preprocess(data)
        if "messages" in processed:
            msg = processed["messages"][0]
            if msg["type"] == "text":
                return msg["text"]["body"]
            if msg["type"] == "interactive":
                if "button_reply" in msg["interactive"]:
                    return msg["interactive"]["button_reply"]["title"]
                if "list_reply" in msg["interactive"]:
                    return msg["interactive"]["list_reply"]["title"]
        return None

    def get_message_id(self, data: Dict[Any, Any]) -> Optional[str]:
        """Returns the ID of the inbound message."""
        processed = self.preprocess(data)
        if "messages" in processed:
            return processed["messages"][0]["id"]
        return None

    def get_message_type(self, data: Dict[Any, Any]) -> Optional[str]:
        """Returns the type of the inbound message (text, image, interactive, etc.)."""
        processed = self.preprocess(data)
        if "messages" in processed:
            return processed["messages"][0]["type"]
        return None

    def get_message_timestamp(self, data: Dict[Any, Any]) -> Optional[str]:
        """Returns the Unix timestamp of the inbound message."""
        processed = self.preprocess(data)
        if "messages" in processed:
            return processed["messages"][0]["timestamp"]
        return None

    def get_interactive_response(self, data: Dict[Any, Any]) -> Optional[Dict]:
        """
        Returns the full interactive response dict.

        Example:
            >>> response = wa.get_interactive_response(data)
            >>> kind = response.get("type")          # "button_reply" or "list_reply"
            >>> item_id = response[kind]["id"]
            >>> item_title = response[kind]["title"]
        """
        processed = self.preprocess(data)
        if "messages" in processed:
            if "interactive" in processed["messages"][0]:
                return processed["messages"][0]["interactive"]
        return None

    def get_image(self, data: Dict[Any, Any]) -> Optional[Dict]:
        """Returns the image dict from an inbound image message."""
        processed = self.preprocess(data)
        if "messages" in processed:
            if "image" in processed["messages"][0]:
                return processed["messages"][0]["image"]
        return None

    def get_delivery(self, data: Dict[Any, Any]) -> Optional[str]:
        """Returns the delivery status from a status update webhook event."""
        processed = self.preprocess(data)
        if "statuses" in processed:
            return processed["statuses"][0]["status"]
        return None

    def get_conversation_id(self, data: Dict[Any, Any]) -> Optional[str]:
        """Returns the conversation ID if present in the webhook payload."""
        processed = self.preprocess(data)
        return processed.get("conversation_id")

    def changed_field(self, data: Dict[Any, Any]) -> str:
        """Returns the changed field name from a webhook event."""
        return data["entry"][0]["changes"][0]["field"]

    # -----------------------------------------------------------------------
    # Internal helpers
    # -----------------------------------------------------------------------

    def _build_list_interactive(self, button: Dict[Any, Any]) -> Dict[Any, Any]:
        """Builds the interactive list payload from a button dict."""
        data: Dict[str, Any] = {"type": "list", "action": button.get("action")}
        if button.get("header"):
            data["header"] = {"type": "text", "text": button["header"]}
        if button.get("body"):
            data["body"] = {"text": button["body"]}
        if button.get("footer"):
            data["footer"] = {"text": button["footer"]}
        return data

    # keep old name for backward compatibility
    def create_button(self, button: Dict[Any, Any]) -> Dict[Any, Any]:
        return self._build_list_interactive(button)


    def is_flow_reply(self, data: Dict) -> bool:
        """Returns True if the webhook is a completed Flow response."""
        processed = self.preprocess(data)
        if "messages" in processed:
            msg = processed["messages"][0]
            if msg.get("type") == "interactive":
                return msg["interactive"].get("type") == "nfm_reply"
        return False

    def get_flow_reply(self, data: Dict) -> Optional[Dict]:
        """
        Extracts the full flow reply data from a completed Flow webhook.

        Returns a dict with:
            - flow_token: the token you set when sending (use this to identify which flow)
            - response: dict of all fields the user submitted
            - from: the user's phone number
            - context_message_id: the original message ID that triggered the flow

        Example:
            >>> if wa.is_flow_reply(data):
            ...     reply = wa.get_flow_reply(data)
            ...     token = reply["flow_token"]      # identifies which flow/session
            ...     name  = reply["response"].get("full_name")
        """
        processed = self.preprocess(data)
        if "messages" not in processed:
            return None

        msg = processed["messages"][0]
        if msg.get("type") != "interactive":
            return None
        if msg["interactive"].get("type") != "nfm_reply":
            return None

        nfm = msg["interactive"]["nfm_reply"]
        response_json = nfm.get("response_json", {})

        # response_json can arrive as a raw JSON string or already parsed dict
        if isinstance(response_json, str):
            import json
            response_json = json.loads(response_json)

        flow_token = response_json.pop("flow_token", None)

        return {
            "flow_token": flow_token,
            "response": response_json,           # all user-submitted fields
            "from": msg.get("from"),
            "context_message_id": msg.get("context", {}).get("id"),
        }