"""OpenAI-compatible ReAct agent with function-calling support."""

from __future__ import annotations

import json
import re
from urllib import error, request
from typing import Any

from email_assistant.config import get_settings
from email_assistant.tools import TOOL_SCHEMAS, execute_tool


class EmailAgent:
    """Autonomous email assistant agent with tool-calling capabilities."""

    MAX_ITERATIONS: int = 10  # Safety cap for the tool-calling loop
    EMAIL_PATTERN = re.compile(
        r"[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}",
        re.IGNORECASE,
    )
    LAST_SENT_PATTERN = re.compile(
        r"\b(?:last|latest)\s+(\d+)\s+(?:sent\s+)?(?:mail|mails|email|emails)\b",
        re.IGNORECASE,
    )

    def __init__(self) -> None:
        settings = get_settings()
        settings.validate()
        self._base_url = settings.llm_base_url
        self._model = settings.llm_model
        self._conversation: list[dict[str, Any]] = [
            {"role": "system", "content": settings.system_prompt},
            {
                "role": "system",
                "content": (
                    "Never fabricate email data. Only report emails returned by tools. "
                    "If a tool was not called or results are empty, clearly say so. "
                    "Never output placeholders like [Insert Date] or [Insert Message ID]."
                ),
            },
        ]

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def ask(self, user_input: str) -> str:
        """Process a natural-language query through the agent loop.

        The agent may call tools multiple times before producing a final
        answer.

        Args:
            user_input: The user's natural-language query.

        Returns:
            The agent's final text response.
        """
        send_response = self._maybe_handle_send_request(user_input)
        if send_response is not None:
            self._conversation.append({"role": "user", "content": user_input})
            self._conversation.append(
                {"role": "assistant", "content": send_response}
            )
            return send_response

        direct_response = self._maybe_handle_direct_sent_mail_request(user_input)
        if direct_response is not None:
            self._conversation.append({"role": "user", "content": user_input})
            self._conversation.append(
                {"role": "assistant", "content": direct_response}
            )
            return direct_response

        self._conversation.append({"role": "user", "content": user_input})

        for _ in range(self.MAX_ITERATIONS):
            response_msg = self._chat_completion()

            # If the model wants to call tools, execute them
            tool_calls = response_msg.get("tool_calls")
            if tool_calls:
                # Append the assistant message (with tool_calls) to history
                self._conversation.append(
                    self._serialise_assistant_message(response_msg)
                )

                for tool_call in tool_calls:
                    result = self._handle_tool_call(tool_call)
                    tool_error = self._extract_tool_error(result)
                    self._conversation.append(
                        {
                            "role": "tool",
                            "tool_call_id": tool_call["id"],
                            "content": result,
                        }
                    )
                    if tool_error:
                        friendly = self._friendly_tool_error(tool_error)
                        self._conversation.append(
                            {"role": "assistant", "content": friendly}
                        )
                        return friendly
                continue  # Let the model see the results and decide next step

            # No tool calls → final text answer
            answer = response_msg.get("content", "") or ""
            self._conversation.append(
                {"role": "assistant", "content": answer}
            )
            return answer

        return (
            "I reached the maximum number of reasoning steps. "
            "Please try a more specific query."
        )

    def _maybe_handle_direct_sent_mail_request(self, user_input: str) -> str | None:
        """Handle common 'last sent email(s)' requests deterministically."""
        text = user_input.strip().lower()
        if "sent" not in text:
            return None
        if not any(token in text for token in ("mail", "mails", "email", "emails")):
            return None
        if "last" not in text and "latest" not in text:
            return None

        count = 10
        match = self.LAST_SENT_PATTERN.search(user_input)
        if match:
            count = max(1, min(50, int(match.group(1))))
        elif any(phrase in text for phrase in ("last mail", "last email", "latest mail", "latest email")):
            count = 1

        raw = execute_tool("search_emails", {"query": "in:sent", "max_results": count})
        try:
            results = json.loads(raw)
        except json.JSONDecodeError:
            return "I couldn't parse Gmail results. Please try again."

        if isinstance(results, dict) and "error" in results:
            return self._friendly_tool_error(str(results["error"]))

        if not isinstance(results, list) or not results:
            return "I couldn't find any sent emails for that request."

        if count == 1:
            latest = results[0]
            subject = latest.get("subject") or "(no subject)"
            sender = latest.get("sender") or "Unknown"
            date = latest.get("date") or "Unknown date"
            snippet = latest.get("snippet") or ""
            return (
                "Here is your latest sent email:\n\n"
                f"- Sender: {sender}\n"
                f"- Subject: {subject}\n"
                f"- Date: {date}\n"
                f"- Snippet: {snippet}"
            )

        lines = [f"Here are your latest {len(results)} sent emails:", ""]
        for index, item in enumerate(results, 1):
            sender = item.get("sender") or "Unknown"
            subject = item.get("subject") or "(no subject)"
            date = item.get("date") or "Unknown date"
            lines.append(f"{index}. {sender}")
            lines.append(f"   Subject: {subject}")
            lines.append(f"   Date: {date}")
            snippet = (item.get("snippet") or "").strip()
            if snippet:
                lines.append(f"   Snippet: {snippet}")
            lines.append("")
        return "\n".join(lines).strip()

    def reset(self) -> None:
        """Clear conversation history (keep system prompt)."""
        self._conversation = [self._conversation[0]]

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

    def _maybe_handle_send_request(self, user_input: str) -> str | None:
        """Handle explicit send-email requests without relying on the LLM."""
        if not self._looks_like_send_request(user_input):
            return None

        email_request = self._extract_send_request(user_input)
        missing_fields = [
            field for field in ("to", "subject", "body") if not email_request[field]
        ]
        if missing_fields:
            friendly_names = {
                "to": "recipient email address",
                "subject": "subject",
                "body": "message body",
            }
            needed = ", ".join(friendly_names[field] for field in missing_fields)
            return f"To send that email, I still need the {needed}."

        tool_result = json.loads(
            execute_tool(
                "send_email",
                {
                    "to": email_request["to"],
                    "subject": email_request["subject"],
                    "body": email_request["body"],
                    "cc": email_request["cc"],
                    "bcc": email_request["bcc"],
                },
            )
        )
        if tool_result.get("status") == "sent":
            return (
                f"Email sent to {email_request['to']} with subject "
                f'"{email_request["subject"]}".'
            )
        return f"I couldn't send the email: {tool_result.get('error', 'unknown error')}"

    @staticmethod
    def _looks_like_send_request(user_input: str) -> bool:
        """Return True for explicit requests to send or compose email."""
        text = user_input.strip().lower()
        return (
            ("send" in text and ("email" in text or "mail" in text))
            or text.startswith("email ")
            or text.startswith("mail ")
        )

    @classmethod
    def _extract_send_request(cls, user_input: str) -> dict[str, str]:
        """Extract recipient, subject, body, cc, and bcc from plain text."""
        return {
            "to": cls._extract_recipients(user_input, "to"),
            "cc": cls._extract_recipients(user_input, "cc"),
            "bcc": cls._extract_recipients(user_input, "bcc"),
            "subject": cls._extract_segment(
                user_input,
                "subject|title",
                ("saying", "say", "body", "message", "cc", "bcc", "to"),
            ),
            "body": cls._extract_segment(
                user_input,
                "saying|say|body|message",
                ("subject", "title", "cc", "bcc", "to"),
            ),
        }

    @classmethod
    def _extract_recipients(cls, user_input: str, field_name: str) -> str:
        """Extract comma-separated email recipients for a field like to/cc/bcc."""
        pattern = re.compile(
            rf"\b{field_name}\s+(.+?)(?=(?:\s+\b(?:to|cc|bcc|subject|title|saying|say|body|message)\b)|$)",
            re.IGNORECASE,
        )
        match = pattern.search(user_input)
        if not match:
            if field_name != "to":
                return ""
            all_addresses = cls.EMAIL_PATTERN.findall(user_input)
            unique_addresses = list(dict.fromkeys(all_addresses))
            return ", ".join(unique_addresses)

        addresses = cls.EMAIL_PATTERN.findall(match.group(1))
        unique_addresses = list(dict.fromkeys(addresses))
        return ", ".join(unique_addresses)

    @staticmethod
    def _extract_segment(
        user_input: str,
        field_pattern: str,
        stop_fields: tuple[str, ...],
    ) -> str:
        """Extract a free-text field such as subject or body."""
        stop_pattern = "|".join(stop_fields)
        pattern = re.compile(
            rf"\b(?:{field_pattern})\b\s*(?:is|as|:)?\s*[\"']?(.*?)[\"']?(?=(?:\s+\b(?:and\s+)?(?:{stop_pattern})\b)|$)",
            re.IGNORECASE,
        )
        match = pattern.search(user_input)
        if not match:
            return ""
        return match.group(1).strip(" \t\n\r\"'.,")

    def _chat_completion(self) -> Any:
        """Call the configured OpenAI-compatible Chat Completions API."""
        payload = {
            "model": self._model,
            "messages": self._conversation,
            "tools": TOOL_SCHEMAS,
            "tool_choice": "auto",
            "temperature": 0.2,
            "stream": False,
        }
        headers = {
            "Content-Type": "application/json",
        }

        http_request = request.Request(
            url=f"{self._base_url}/chat/completions",
            data=json.dumps(payload).encode("utf-8"),
            headers=headers,
            method="POST",
        )
        try:
            with request.urlopen(http_request) as response:
                data = json.loads(response.read().decode("utf-8"))
        except error.HTTPError as exc:
            body = exc.read().decode("utf-8", errors="replace")
            raise RuntimeError(
                f"LLM request failed with HTTP {exc.code}: {body}"
            ) from exc
        except error.URLError as exc:
            raise RuntimeError(
                f"Could not reach local LLM server at {self._base_url}: {exc.reason}"
            ) from exc

        choices = data.get("choices")
        if not isinstance(choices, list) or not choices:
            raise RuntimeError(
                "LLM response did not include choices. "
                f"Server response: {json.dumps(data, ensure_ascii=False)}"
            )

        first = choices[0]
        message = first.get("message") if isinstance(first, dict) else None
        if not isinstance(message, dict):
            raise RuntimeError(
                "LLM response did not include a valid message object. "
                f"Server response: {json.dumps(data, ensure_ascii=False)}"
            )

        return message

    @staticmethod
    def _extract_tool_error(tool_result: str) -> str | None:
        """Extract error text from tool JSON output when present."""
        try:
            parsed = json.loads(tool_result)
        except json.JSONDecodeError:
            return None

        if isinstance(parsed, dict):
            error_value = parsed.get("error")
            if isinstance(error_value, str) and error_value.strip():
                return error_value.strip()
        return None

    @staticmethod
    def _friendly_tool_error(raw_error: str) -> str:
        """Convert low-level tool errors into user-friendly guidance."""
        lowered = raw_error.lower()

        if "insufficient authentication scopes" in lowered or "insufficientpermissions" in lowered:
            return (
                "Your Google token is missing required Gmail permissions. "
                "Run /setup, delete the token file shown there, restart MixMail, "
                "and complete Google sign-in again."
            )
        if "invalid_scope" in lowered:
            return (
                "Google rejected the requested Gmail scope. MixMail now uses "
                "read-only scope by default. Delete the token file shown in /setup, "
                "restart MixMail, and sign in again."
            )
        if "invalid_grant" in lowered or "token" in lowered and "expired" in lowered:
            return (
                "Your Gmail token is invalid or expired. Run /setup, delete the "
                "token file, then sign in again when prompted."
            )
        return f"Gmail tool error: {raw_error}"

    @staticmethod
    def _handle_tool_call(tool_call: Any) -> str:
        """Execute a single tool call and return the result as a string."""
        function = tool_call["function"]
        name = function["name"]
        arguments = function["arguments"]
        return execute_tool(name, arguments)

    @staticmethod
    def _serialise_assistant_message(msg: Any) -> dict[str, Any]:
        """Convert an assistant message object to a serialisable dict.

        Required because the conversation list stores plain dicts, not
        client objects.
        """
        serialised: dict[str, Any] = {
            "role": "assistant",
            "content": msg.get("content"),
        }
        tool_calls = msg.get("tool_calls")
        if tool_calls:
            serialised["tool_calls"] = [
                {
                    "id": tc["id"],
                    "type": "function",
                    "function": {
                        "name": tc["function"]["name"],
                        "arguments": tc["function"]["arguments"],
                    },
                }
                for tc in tool_calls
            ]
        return serialised
