import logging
import os
import re
from email.message import MIMEPart
from email.utils import formataddr, make_msgid
from urllib.parse import unquote

from django.core.mail import get_connection
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from waffle import switch_is_active
from anymail.message import AnymailMessage
from anymail.exceptions import AnymailAPIError

from api.models import (
    AgentEmailAccount,
    AgentFsNode,
    CommsChannel,
    DeliveryStatus,
    OutboundMessageAttempt,
    PersistentAgentEmailEndpoint,
    PersistentAgentMessage,
)
from api.services.system_settings import get_max_file_size
from api.agent.files.attachment_helpers import track_file_send_failed, track_file_unsupported
from opentelemetry.trace import get_current_span
from opentelemetry import trace
from django.template.loader import render_to_string

from util import sms
from util.analytics import Analytics, AnalyticsEvent, AnalyticsSource
from util.integrations import postmark_status
from util.text_sanitizer import decode_unicode_escapes, normalize_llm_output

from .cid_references import CID_SRC_REFERENCE_RE
from .email_content import convert_body_to_html_and_plaintext
from .email_footer_service import append_footer_if_needed
from .email_threading import build_reply_headers, get_message_raw_payload, get_message_rfc_message_id
from .smtp_transport import SmtpTransport


# ──────────────────────────────────────────────────────────────────────────────
# SMS Content Conversion Helper
# ──────────────────────────────────────────────────────────────────────────────


def _convert_sms_body_to_plaintext(body: str) -> str:
    """Detect whether *body* is HTML, Markdown, or plaintext and return a
    plaintext string suitable for SMS. The detection and conversion process
    closely mirrors the email conversion logic but targets a single plaintext
    output.

    Steps:
    1. If any common HTML tag is present, strip to text via *inscriptis*.
    2. Else if Markdown syntax is detected, render to plaintext via *pypandoc*.
    3. Otherwise, treat as generic plaintext.

    All stages emit detailed INFO-level logs for observability.
    """
    import re
    import logging
    from inscriptis import get_text  # type: ignore
    from inscriptis.model.config import ParserConfig  # type: ignore
    from inscriptis.css_profiles import CSS_PROFILES  # type: ignore
    import pypandoc  # type: ignore

    logger = logging.getLogger(__name__)

    # Normalize LLM output first: decode escape sequences, strip control chars
    # This handles cases where LLMs output \u2014 instead of —, \n instead of newlines, etc.
    body = normalize_llm_output(body)

    body_length = len(body)
    body_preview = body[:200] + "..." if len(body) > 200 else body
    logger.info(
        "SMS content conversion starting. Input body length: %d characters. Preview: %r",
        body_length,
        body_preview,
    )

    # ------------------------------------------------------------------ Detect HTML
    logger.info("=== SMS HTML DETECTION START ===")
    html_tag_pattern = r"</?(?:p|br|div|span|a|ul|ol|li|h[1-6]|strong|em|b|i|code|pre|blockquote|table|thead|tbody|tr|th|td)\b[^>]*>"
    html_match = re.search(html_tag_pattern, body, re.IGNORECASE)
    
    if html_match:
        logger.info("=== HTML DETECTED ===")
        logger.info(
            "Content type: HTML. Found tag %r at position %d",
            html_match.group(0),
            html_match.start(),
        )
        
        # Show context around the match
        match_start = max(0, html_match.start() - 20)
        match_end = min(len(body), html_match.end() + 20)
        context = body[match_start:match_end]
        logger.info("HTML tag context: %r", context)
        
        logger.info("=== INSCRIPTIS CONVERSION START ===")
        logger.info("Input to inscriptis (length=%d):\n%s", len(body), body)
        
        strict_css = CSS_PROFILES['strict'].copy()
        config = ParserConfig(css=strict_css, display_links=True, display_anchors=True)
        logger.info("Inscriptis config: css=strict, display_links=True, display_anchors=True")
        
        raw_output = get_text(body, config)
        logger.info("=== INSCRIPTIS RAW OUTPUT ===")
        logger.info("Raw inscriptis output (before .strip()):\n%r", raw_output)
        
        plaintext = raw_output.strip()
        logger.info("=== INSCRIPTIS FINAL OUTPUT ===")
        logger.info("Final output after .strip() (length=%d):\n%r", len(plaintext), plaintext)
        
        logger.info(
            "✓ HTML → plaintext conversion SUCCESSFUL. Input length: %d → Output length: %d",
            len(body),
            len(plaintext)
        )
        logger.info("=== SMS HTML CONVERSION COMPLETE ===")
        return plaintext
    else:
        logger.info("✗ No HTML tags detected, proceeding to markdown detection")

    # ------------------------------------------------------------------ Detect Markdown
    markdown_patterns = [
        (r"^\s{0,3}#", "heading"),                 # Heading
        (r"\*\*.+?\*\*", "bold_asterisk"),        # Bold **text**
        (r"__.+?__", "bold_underscore"),          # Bold __text__
        (r"`{1,3}.+?`{1,3}", "code"),             # Inline/fenced code
        (r"\[[^\]]+\]\([^)]+\)", "link"),         # Link [text](url)
        (r"^\s*[-*+] ", "unordered_list"),        # Unordered list
        (r"^\s*\d+\. ", "ordered_list"),          # Ordered list
    ]
    
    # Detailed pattern analysis
    detected_patterns = []
    logger.info("=== SMS MARKDOWN PATTERN DETECTION START ===")
    logger.info("Analyzing input body for markdown patterns...")
    logger.info("Input body (full):\n%r", body)
    
    for pattern, pattern_name in markdown_patterns:
        matches = list(re.finditer(pattern, body, flags=re.MULTILINE))
        if matches:
            detected_patterns.append((pattern_name, len(matches)))
            logger.info(
                "✓ PATTERN MATCH: '%s' (%s) found %d times",
                pattern_name,
                pattern,
                len(matches)
            )
            for i, match in enumerate(matches):
                logger.info(
                    "  Match %d: %r at position %d-%d (line context: %r)",
                    i + 1,
                    match.group(0),
                    match.start(),
                    match.end(),
                    body[max(0, match.start()-20):match.end()+20]
                )
        else:
            logger.info("✗ No match: '%s' (%s)", pattern_name, pattern)
    
    if detected_patterns:
        logger.info("=== MARKDOWN DETECTED ===")
        logger.info(
            "Content type: MARKDOWN. Detected patterns: %s",
            ", ".join([f"{name}({count})" for name, count in detected_patterns])
        )
        
        logger.info("=== PYPANDOC CONVERSION START ===")
        logger.info("Input to pypandoc (length=%d):", len(body))
        logger.info("Input content:\n%s", body)
        
        logger.info("Pypandoc args: to='plain', format='gfm', extra_args=['--wrap=preserve', '--reference-links']")
        
        try:
            # Convert markdown to plaintext using pypandoc
            # Using GFM format with --wrap=preserve to properly handle list formatting
            # and --reference-links to preserve link formatting
            raw_output = pypandoc.convert_text(
                body,
                to="plain",
                format="gfm",
                extra_args=["--wrap=preserve", "--reference-links"]
            )
            logger.info("=== PYPANDOC RAW OUTPUT ===")
            logger.info("Raw pypandoc output (before .strip()):\n%r", raw_output)
            logger.info("Raw pypandoc output formatted:\n%s", raw_output)
            
            plaintext = raw_output.strip()
            logger.info("=== PYPANDOC FINAL OUTPUT ===")
            logger.info("Final output after .strip() (length=%d):\n%r", len(plaintext), plaintext)
            logger.info("Final output formatted:\n%s", plaintext)
            
            # Character-by-character comparison for debugging
            if body != plaintext:
                logger.info("=== INPUT vs OUTPUT COMPARISON ===")
                logger.info("Input chars: %r", [c for c in body])
                logger.info("Output chars: %r", [c for c in plaintext])
                
                # Line-by-line comparison
                input_lines = body.split('\n')
                output_lines = plaintext.split('\n')
                logger.info("Input lines (%d): %r", len(input_lines), input_lines)
                logger.info("Output lines (%d): %r", len(output_lines), output_lines)
                
                for i, (input_line, output_line) in enumerate(zip(input_lines, output_lines)):
                    if input_line != output_line:
                        logger.info("Line %d changed: %r → %r", i, input_line, output_line)
            
            logger.info(
                "✓ Markdown → plaintext conversion SUCCESSFUL. Input length: %d → Output length: %d",
                len(body),
                len(plaintext)
            )
            logger.info("=== SMS MARKDOWN CONVERSION COMPLETE ===")
            return plaintext
            
        except Exception as e:
            logger.error("=== PYPANDOC CONVERSION FAILED ===")
            logger.error("Exception type: %s", type(e).__name__)
            logger.error("Exception message: %s", str(e))
            logger.error("Falling back to original markdown text (stripped)")
            fallback = body.strip()
            logger.info("Fallback output: %r", fallback)
            return fallback

    # ------------------------------------------------------------------ Plaintext fallback
    logger.info("=== SMS PLAINTEXT FALLBACK ===")
    logger.info("No markdown patterns detected. Treating as plaintext.")
    logger.info("Content type detected for SMS: Plaintext. No HTML or Markdown patterns found.")
    fallback = body.strip()
    logger.info("Plaintext output (after .strip(), length=%d): %r", len(fallback), fallback)
    logger.info("=== SMS PLAINTEXT CONVERSION COMPLETE ===")
    return fallback

tracer = trace.get_tracer("gobii.utils")
logger = logging.getLogger(__name__)


_postmark_connection = None
_CID_UNSAFE_CHARS_RE = re.compile(r"[^a-z0-9._-]+")


def _get_postmark_connection():
    """Return a reusable Postmark connection when integration is enabled."""
    global _postmark_connection
    if _postmark_connection is not None:
        return _postmark_connection
    if not postmark_status().enabled:
        return None
    _postmark_connection = get_connection("anymail.backends.postmark.EmailBackend")
    return _postmark_connection


def _prepare_email_content(message: PersistentAgentMessage, body_raw: str) -> tuple[str, str]:
    """Convert the raw body into HTML/plaintext and append footers when required."""
    html_snippet, plaintext_body = convert_body_to_html_and_plaintext(body_raw)
    agent = getattr(message, "owner_agent", None)
    return append_footer_if_needed(agent, html_snippet, plaintext_body)


def _should_suppress_display_name(from_endpoint) -> bool:
    if from_endpoint is None:
        return False
    try:
        account = from_endpoint.agentemailaccount
    except AgentEmailAccount.DoesNotExist:
        return False
    if (
        account.is_outbound_enabled
        or account.is_inbound_enabled
        or account.smtp_auth == AgentEmailAccount.AuthMode.OAUTH2
        or account.imap_auth == AgentEmailAccount.ImapAuthMode.OAUTH2
        or any([
            account.smtp_host,
            account.imap_host,
            account.smtp_username,
            account.imap_username,
            account.smtp_port,
            account.imap_port,
        ])
    ):
        return True
    try:
        account.oauth_credential
        return True
    except ObjectDoesNotExist:
        return False


def _build_from_header(message: PersistentAgentMessage) -> str:
    from_endpoint = getattr(message, "from_endpoint", None)
    from_address = (getattr(from_endpoint, "address", None) or "").strip()
    if not from_address:
        return ""
    if _should_suppress_display_name(from_endpoint):
        return from_address
    display_name = ""
    if from_endpoint is not None:
        try:
            email_meta = from_endpoint.email_meta
            display_name = (getattr(email_meta, "display_name", "") or "").strip()
        except PersistentAgentEmailEndpoint.DoesNotExist:
            pass
    display_name = display_name.replace("\r", "").replace("\n", "").strip()
    if display_name:
        return formataddr((display_name, from_address))
    return from_address


def _normalized_email_subject(message: PersistentAgentMessage) -> str:
    """Return subject with escaped unicode sequences decoded for delivery."""
    raw_payload = message.raw_payload if isinstance(message.raw_payload, dict) else {}
    raw_subject = raw_payload.get("subject", "")
    if raw_subject is None:
        return ""
    return decode_unicode_escapes(str(raw_subject))


def _get_email_primary_recipient(message: PersistentAgentMessage) -> str:
    if message.to_endpoint and message.to_endpoint.address:
        return message.to_endpoint.address
    if message.conversation and message.conversation.address:
        return message.conversation.address
    return ""


def _persist_outbound_email_threading_metadata(message: PersistentAgentMessage) -> tuple[str, dict[str, str]]:
    raw_payload = get_message_raw_payload(message)
    next_payload = dict(raw_payload)
    message_rfc_id = get_message_rfc_message_id(message)
    if not message_rfc_id:
        message_rfc_id = make_msgid()
        next_payload["message_id"] = message_rfc_id

    reply_headers = build_reply_headers(getattr(message, "parent", None))
    in_reply_to = reply_headers.get("In-Reply-To", "").strip()
    references = reply_headers.get("References", "").strip()
    if in_reply_to:
        next_payload["in_reply_to"] = in_reply_to
    if references:
        next_payload["references"] = references

    if next_payload != raw_payload:
        message.raw_payload = next_payload
        message.save(update_fields=["raw_payload"])

    return message_rfc_id, reply_headers


def _extract_cid_references(html_body: str) -> list[dict[str, int | str]]:
    if not html_body:
        return []

    references: list[dict[str, int | str]] = []
    for match in CID_SRC_REFERENCE_RE.finditer(html_body):
        raw_value = (match.group("dq") or match.group("sq") or match.group("bare") or "").strip()
        if not raw_value.lower().startswith("cid:"):
            continue
        raw_cid = raw_value[4:].strip()
        if not raw_cid:
            continue
        quote = '"' if match.group("dq") is not None else "'" if match.group("sq") is not None else ""
        references.append(
            {
                "start": match.start(),
                "end": match.end(),
                "prefix": match.group("prefix") or "src=",
                "quote": quote,
                "raw_cid": raw_cid,
            }
        )
    return references


def _build_cid_reference_lookup(
    references: list[dict[str, int | str]],
) -> tuple[dict[str, list[int]], dict[str, list[int]]]:
    cid_lookup: dict[str, list[int]] = {}
    basename_lookup: dict[str, list[int]] = {}
    for index, reference in enumerate(references):
        raw_cid = str(reference["raw_cid"]).strip()
        if not raw_cid:
            continue
        cid_variants = [raw_cid]
        decoded_cid = unquote(raw_cid).strip()
        if decoded_cid and decoded_cid != raw_cid:
            cid_variants.append(decoded_cid)

        for cid_variant in cid_variants:
            normalized = cid_variant.lower()
            cid_candidates = cid_lookup.setdefault(normalized, [])
            if not cid_candidates or cid_candidates[-1] != index:
                cid_candidates.append(index)

            basename = os.path.basename(normalized).strip()
            if basename:
                basename_candidates = basename_lookup.setdefault(basename, [])
                if not basename_candidates or basename_candidates[-1] != index:
                    basename_candidates.append(index)
    return cid_lookup, basename_lookup


def _pop_next_reference_index(candidates: list[int], used_reference_indexes: set[int]) -> int | None:
    while candidates:
        candidate = candidates.pop(0)
        if candidate not in used_reference_indexes:
            return candidate
    return None


def _build_cid_variants(raw_cid: str) -> list[str]:
    normalized_raw = raw_cid.strip().lower()
    if not normalized_raw:
        return []
    variants = [normalized_raw]
    decoded = unquote(raw_cid).strip().lower()
    if decoded and decoded != normalized_raw:
        variants.append(decoded)
    return variants


def _canonicalize_inline_cid(raw_cid: str, filename: str, reference_index: int) -> str:
    base = unquote(raw_cid).strip() or filename.strip() or "attachment"
    normalized = _CID_UNSAFE_CHARS_RE.sub("-", base.lower()).strip("-.")
    if not normalized:
        normalized = "attachment"
    if len(normalized) > 80:
        normalized = normalized[:80].rstrip("-.") or "attachment"
    return f"inline-{reference_index + 1}-{normalized}"


def _rewrite_inline_cid_references(
    html_body: str,
    references: list[dict[str, int | str]],
    cid_replacements: dict[int, str],
) -> str:
    if not html_body or not references or not cid_replacements:
        return html_body

    parts: list[str] = []
    cursor = 0
    for index, reference in enumerate(references):
        start = int(reference["start"])
        end = int(reference["end"])
        parts.append(html_body[cursor:start])
        replacement_cid = cid_replacements.get(index)
        if replacement_cid:
            prefix = str(reference["prefix"])
            quote = str(reference["quote"])
            if quote:
                parts.append(f"{prefix}{quote}cid:{replacement_cid}{quote}")
            else:
                parts.append(f"{prefix}cid:{replacement_cid}")
        else:
            parts.append(html_body[start:end])
        cursor = end
    parts.append(html_body[cursor:])
    return "".join(parts)


def _to_mime_type_parts(content_type: str) -> tuple[str, str]:
    base_content_type = (content_type or "").split(";", 1)[0].strip().lower()
    if "/" not in base_content_type:
        return "application", "octet-stream"
    maintype, subtype = base_content_type.split("/", 1)
    return maintype or "application", subtype or "octet-stream"


def _attach_email_attachments(message: PersistentAgentMessage, msg: AnymailMessage, html_body: str) -> tuple[int, str]:
    attachments = list(message.attachments.select_related("filespace_node"))
    if not attachments:
        return 0, html_body

    cid_references = _extract_cid_references(html_body)
    cid_lookup, basename_cid_lookup = _build_cid_reference_lookup(cid_references)
    agent = getattr(message, "owner_agent", None)
    message_id = str(getattr(message, "id", "")) if getattr(message, "id", None) else None
    channel = getattr(getattr(message, "from_endpoint", None), "channel", None)
    user_initiated = bool(getattr(agent, "user_id", None)) if agent else None
    max_bytes = get_max_file_size()
    attached = 0
    used_reference_indexes: set[int] = set()
    cid_replacements: dict[int, str] = {}
    for att in attachments:
        filename = att.filename or "attachment"
        content_type = att.content_type or "application/octet-stream"
        file_field = None

        if att.file and getattr(att.file, "name", None):
            file_field = att.file
        else:
            node = getattr(att, "filespace_node", None)
            if node and getattr(node, "node_type", None) == AgentFsNode.NodeType.FILE:
                file_field = node.content
                if node.name:
                    filename = node.name
                if node.mime_type:
                    content_type = node.mime_type or content_type

        if not file_field or not getattr(file_field, "name", None):
            logger.warning("Skipping attachment %s for message %s (missing file content)", att.id, message.id)
            track_file_send_failed(
                agent,
                node=getattr(att, "filespace_node", None),
                path=getattr(getattr(att, "filespace_node", None), "path", None),
                filename=filename,
                size_bytes=getattr(att, "file_size", None),
                mime_type=content_type,
                channel=channel,
                message_id=message_id,
                reason_code="missing_blob",
                user_initiated=user_initiated,
            )
            continue

        size_bytes = att.file_size or getattr(file_field, "size", None)
        try:
            if max_bytes and size_bytes and int(size_bytes) > int(max_bytes):
                logger.warning(
                    "Skipping attachment %s for message %s (size %s exceeds max %s)",
                    att.id,
                    message.id,
                    size_bytes,
                    max_bytes,
                )
                track_file_unsupported(
                    agent,
                    node=getattr(att, "filespace_node", None),
                    path=getattr(getattr(att, "filespace_node", None), "path", None),
                    filename=filename,
                    size_bytes=int(size_bytes),
                    mime_type=content_type,
                    channel=channel,
                    message_id=message_id,
                    reason_code="too_large",
                    user_initiated=user_initiated,
                )
                continue
        except (TypeError, ValueError):
            logger.warning(
                "Skipping attachment %s for message %s (failed size validation)",
                att.id,
                message.id,
            )
            track_file_send_failed(
                agent,
                node=getattr(att, "filespace_node", None),
                path=getattr(getattr(att, "filespace_node", None), "path", None),
                filename=filename,
                size_bytes=None,
                mime_type=content_type,
                channel=channel,
                message_id=message_id,
                reason_code="validation_failed",
                user_initiated=user_initiated,
            )
            continue

        storage = file_field.storage
        name = file_field.name
        if hasattr(storage, "exists") and not storage.exists(name):
            logger.warning("Skipping attachment %s for message %s (missing storage blob)", att.id, message.id)
            track_file_send_failed(
                agent,
                node=getattr(att, "filespace_node", None),
                path=getattr(getattr(att, "filespace_node", None), "path", None),
                filename=filename,
                size_bytes=size_bytes if isinstance(size_bytes, int) else None,
                mime_type=content_type,
                channel=channel,
                message_id=message_id,
                reason_code="missing_blob",
                user_initiated=user_initiated,
            )
            continue

        try:
            with storage.open(name, "rb") as handle:
                content = handle.read()
            matched_reference_index = None
            normalized_filename = filename.strip().lower()
            if normalized_filename:
                cid_candidates = cid_lookup.get(normalized_filename, [])
                matched_reference_index = _pop_next_reference_index(cid_candidates, used_reference_indexes)
                if matched_reference_index is None:
                    basename = os.path.basename(normalized_filename)
                    basename_candidates = basename_cid_lookup.get(basename, [])
                    matched_reference_index = _pop_next_reference_index(
                        basename_candidates,
                        used_reference_indexes,
                    )

            if matched_reference_index is not None:
                raw_cid = str(cid_references[matched_reference_index]["raw_cid"])
                canonical_cid = _canonicalize_inline_cid(raw_cid, filename, matched_reference_index)
                maintype, subtype = _to_mime_type_parts(content_type)
                inline_part = MIMEPart()
                inline_part.set_content(
                    content,
                    maintype=maintype,
                    subtype=subtype,
                    disposition="inline",
                    filename=filename,
                )
                inline_part["Content-ID"] = f"<{canonical_cid}>"
                msg.attach(inline_part)
                # A repeated CID token in HTML should resolve to the same inline attachment everywhere.
                matched_indexes = {matched_reference_index}
                for cid_variant in _build_cid_variants(raw_cid):
                    cid_candidates = cid_lookup.get(cid_variant, [])
                    while cid_candidates:
                        duplicate_index = _pop_next_reference_index(cid_candidates, used_reference_indexes)
                        if duplicate_index is None:
                            break
                        matched_indexes.add(duplicate_index)

                for matched_index in matched_indexes:
                    used_reference_indexes.add(matched_index)
                    cid_replacements[matched_index] = canonical_cid
            else:
                msg.attach(filename, content, content_type)
            attached += 1
        except Exception:
            logger.exception("Failed attaching file %s to message %s", att.id, message.id)
            track_file_send_failed(
                agent,
                node=getattr(att, "filespace_node", None),
                path=getattr(getattr(att, "filespace_node", None), "path", None),
                filename=filename,
                size_bytes=size_bytes if isinstance(size_bytes, int) else None,
                mime_type=content_type,
                channel=channel,
                message_id=message_id,
                reason_code="validation_failed",
                user_initiated=user_initiated,
            )

    rewritten_html_body = _rewrite_inline_cid_references(html_body, cid_references, cid_replacements)
    return attached, rewritten_html_body


@tracer.start_as_current_span("AGENT - Deliver Agent Email")
def deliver_agent_email(message: PersistentAgentMessage):
    """
    Sends an agent's email message using Postmark and updates its status.
    """
    span = get_current_span()

    if not message.is_outbound or message.from_endpoint.channel != CommsChannel.EMAIL:
        logger.warning(
            "deliver_agent_email called for non-outbound or non-email message %s. Skipping.",
            message.id,
        )
        return

    if message.latest_status != DeliveryStatus.QUEUED:
        logger.info(
            "Skipping email delivery for message %s because its status is '%s', not 'queued'.",
            message.id,
            message.latest_status,
        )
        return
    subject = _normalized_email_subject(message)
    to_address = _get_email_primary_recipient(message)
    message_rfc_id, reply_headers = _persist_outbound_email_threading_metadata(message)

    # First: per-endpoint SMTP override
    acct = None
    try:
        # Use a direct DB check to avoid stale related-object caches
        acct = (
            AgentEmailAccount.objects.select_related("endpoint")
            .filter(endpoint=message.from_endpoint, is_outbound_enabled=True)
            .first()
        )
    except Exception:
        acct = None

    if acct is not None:
        logger.info(
            "Using per-endpoint SMTP for message %s from %s",
            message.id,
            message.from_endpoint.address,
        )
        # Mark sending and create attempt for SMTP
        message.latest_status = DeliveryStatus.SENDING
        message.save(update_fields=["latest_status"])

        attempt = OutboundMessageAttempt.objects.create(
            message=message,
            provider="smtp",
            status=DeliveryStatus.SENDING,
        )

        try:
            from_address = message.from_endpoint.address
            body_raw = message.body

            # content conversion
            html_snippet, plaintext_body = _prepare_email_content(message, body_raw)
            html_body = render_to_string(
                "emails/persistent_agent_email.html",
                {"body": html_snippet},
            )

            # Collect all recipients (To + CC)
            recipient_list = [to_address] if to_address else []
            if message.cc_endpoints.exists():
                recipient_list.extend(list(message.cc_endpoints.values_list("address", flat=True)))

            with tracer.start_as_current_span("SMTP Transport Send") as smtp_span:
                smtp_span.set_attribute("from", from_address)
                smtp_span.set_attribute("to_count", 1)
                try:
                    cc_count = message.cc_endpoints.count()
                except Exception:
                    cc_count = 0
                smtp_span.set_attribute("cc_count", cc_count)
                smtp_span.set_attribute("recipient_total", len(recipient_list))
                provider_id = SmtpTransport.send(
                    account=acct,
                    from_addr=from_address,
                    to_addrs=recipient_list,
                    subject=subject,
                    plaintext_body=plaintext_body,
                    html_body=html_body,
                    attempt_id=str(attempt.id),
                    message_id=message_rfc_id,
                    in_reply_to=reply_headers.get("In-Reply-To"),
                    references=reply_headers.get("References"),
                )

            now = timezone.now()
            attempt.status = DeliveryStatus.SENT
            attempt.provider_message_id = provider_id or ""
            attempt.sent_at = now
            attempt.save(update_fields=["status", "provider_message_id", "sent_at"])

            message.latest_status = DeliveryStatus.SENT
            message.latest_sent_at = now
            message.latest_error_message = ""
            message.save(update_fields=["latest_status", "latest_sent_at", "latest_error_message"])

            if span is not None and getattr(span, "is_recording", lambda: False)():
                span.add_event(
                    'Email - SMTP Delivery',
                    {
                        'message_id': str(message.id),
                        'from_address': from_address,
                        'to_address': to_address,
                    },
                )

            props = Analytics.with_org_properties(
                {
                    'agent_id': str(message.owner_agent_id),
                    'message_id': str(message.id),
                    'from_address': from_address,
                    'to_address': to_address,
                    'subject': subject,
                    'provider': 'smtp',
                },
                organization=getattr(message.owner_agent, "organization", None),
            )
            Analytics.track_event(
                user_id=message.owner_agent.user.id,
                event=AnalyticsEvent.PERSISTENT_AGENT_EMAIL_SENT,
                source=AnalyticsSource.AGENT,
                properties=props.copy(),
            )
            return

        except Exception as e:
            logger.exception(
                "SMTP error sending message %s from %r to %r",
                message.id,
                getattr(message.from_endpoint, 'address', None),
                to_address,
            )
            error_str = str(e)
            attempt.status = DeliveryStatus.FAILED
            attempt.error_message = error_str
            attempt.save(update_fields=["status", "error_message"])

            message.latest_status = DeliveryStatus.FAILED
            message.latest_error_message = error_str
            message.save(update_fields=["latest_status", "latest_error_message"])
            return

    # Check environment and token once up front (Postmark or simulation)
    postmark_state = postmark_status()
    postmark_token = os.getenv("POSTMARK_SERVER_TOKEN")
    release_env = getattr(settings, "GOBII_RELEASE_ENV", os.getenv("GOBII_RELEASE_ENV", "local"))
    missing_token = (not postmark_token) or not postmark_state.enabled
    simulation_flag = getattr(settings, "SIMULATE_EMAIL_DELIVERY", False)

    # Simulate only when explicitly enabled and Postmark is not configured.
    # SMTP (per-endpoint) was handled above and takes precedence when present.
    if simulation_flag and missing_token:
        # Tailor message to reason for simulation (explicit flag vs missing token)
        if release_env != "prod" and missing_token:
            logger.info(
                "Running in non-prod environment without POSTMARK_SERVER_TOKEN. Simulating email delivery for message %s.",
                message.id,
            )
        else:
            logger.info(
                "SIMULATE_EMAIL_DELIVERY enabled in %s (POSTMARK_SERVER_TOKEN %s). Simulating email delivery for message %s.",
                release_env,
                "present" if postmark_token else "missing",
                message.id,
            )
        body_raw = message.body
        html_snippet, plaintext_body = _prepare_email_content(message, body_raw)

        # Log simulated content details for parity with non-prod simulation branch
        logger.info(
            "--- SIMULATED EMAIL ---\nFrom: %s\nTo: %s\nSubject: %s\n\n=== ORIGINAL RAW BODY ===\n%s\n\n=== CONVERTED HTML VERSION ===\n%s\n\n=== CONVERTED PLAINTEXT VERSION ===\n%s\n-----------------------",
            _build_from_header(message),
            to_address,
            subject,
            message.body,
            html_snippet,
            plaintext_body,
        )

        now = timezone.now()
        OutboundMessageAttempt.objects.create(
            message=message,
            provider="postmark_simulation",
            status=DeliveryStatus.DELIVERED,
            sent_at=now,
            delivered_at=now,
        )
        message.latest_status = DeliveryStatus.DELIVERED
        message.latest_sent_at = now
        message.latest_delivered_at = now
        message.latest_error_message = ""
        message.save(
            update_fields=["latest_status", "latest_sent_at", "latest_delivered_at", "latest_error_message"]
        )
        if span is not None and getattr(span, "is_recording", lambda: False)():
            span.add_event('Email - Simulated Delivery (flag)', {
                'message_id': str(message.id),
                'from_address': message.from_endpoint.address,
                'to_address': to_address,
            })
        return

    if release_env != "prod" and not postmark_token:
        logger.info(
            "Running in non-prod environment without POSTMARK_SERVER_TOKEN. "
            "Simulating email delivery for message %s.",
            message.id,
        )
        body_raw = message.body
        
        # Log raw message details for simulation as well
        logger.info(
            "SIMULATION - Raw agent message details for message %s: "
            "from=%r, to=%r, subject=%r, body_length=%d",
            message.id,
            message.from_endpoint.address,
            to_address,
            subject,
            len(body_raw)
        )
        
        # For simulation, also show content conversion results
        logger.info("SIMULATION - Processing content conversion for message %s", message.id)
        html_snippet, plaintext_body = _prepare_email_content(message, body_raw)
        
        logger.info(
            "SIMULATION - Content conversion results for message %s: "
            "HTML snippet length: %d, plaintext length: %d",
            message.id,
            len(html_snippet),
            len(plaintext_body)
        )
        
        logger.info(
            "--- SIMULATED EMAIL ---\n"
            "From: %s\nTo: %s\nSubject: %s\n\n"
            "=== ORIGINAL RAW BODY ===\n%s\n\n"
            "=== CONVERTED HTML VERSION ===\n%s\n\n"
            "=== CONVERTED PLAINTEXT VERSION ===\n%s\n"
            "-----------------------",
            _build_from_header(message),
            to_address,
            subject,
            message.body,
            html_snippet,
            plaintext_body,
        )

        now = timezone.now()
        OutboundMessageAttempt.objects.create(
            message=message,
            provider="postmark_simulation",
            status=DeliveryStatus.DELIVERED,
            sent_at=now,
            delivered_at=now,
        )
        message.latest_status = DeliveryStatus.DELIVERED
        message.latest_sent_at = now
        message.latest_delivered_at = now
        message.latest_error_message = ""
        message.save(
            update_fields=["latest_status", "latest_sent_at", "latest_delivered_at", "latest_error_message"]
        )

        if span is not None and getattr(span, "is_recording", lambda: False)():
            span.add_event('Email - Simulated Delivery', {
                'message_id': str(message.id),
                'from_address': message.from_endpoint.address,
                'to_address': to_address,
            })

        return

    # Start by creating an attempt record and updating message status
    message.latest_status = DeliveryStatus.SENDING
    message.save(update_fields=["latest_status"])
    
    attempt = OutboundMessageAttempt.objects.create(
        message=message,
        provider="postmark",
        status=DeliveryStatus.SENDING,
    )

    try:
        from_address = message.from_endpoint.address
        from_header = _build_from_header(message)
        body_raw = message.body
        
        # Log the raw message received from the agent
        logger.info(
            "Processing email message %s. Raw agent message details: "
            "from=%r, to=%r, subject=%r, body_length=%d, raw_payload_keys=%s",
            message.id,
            from_address,
            to_address, 
            subject,
            len(body_raw),
            list(message.raw_payload.keys())
        )
        
        # Log the complete raw body for debugging
        logger.info(
            "Raw message body for message %s: %r",
            message.id,
            body_raw
        )

        # Detect content type and convert appropriately
        logger.info("Starting content type detection and conversion for message %s", message.id)
        html_snippet, plaintext_body = _prepare_email_content(message, body_raw)
        
        # Log the conversion results
        logger.info(
            "Content conversion completed for message %s. HTML snippet length: %d, plaintext length: %d",
            message.id,
            len(html_snippet),
            len(plaintext_body)
        )

        # Wrap with our mobile-first template
        logger.info("Wrapping HTML snippet with email template for message %s", message.id)
        html_body = render_to_string(
            "emails/persistent_agent_email.html",
            {
                "body": html_snippet,
            },
        )
        
        # Log the final template-wrapped HTML
        logger.info(
            "Email template rendering complete for message %s. Final HTML body length: %d",
            message.id,
            len(html_body)
        )
        
        # Log the final message versions (with length limits for readability)
        final_plaintext_preview = plaintext_body[:500] + "..." if len(plaintext_body) > 500 else plaintext_body
        final_html_preview = html_body[:500] + "..." if len(html_body) > 500 else html_body
        
        logger.info(
            "Final email content for message %s - PLAINTEXT VERSION (length: %d): %r",
            message.id,
            len(plaintext_body),
            final_plaintext_preview
        )
        
        logger.info(
            "Final email content for message %s - HTML VERSION (length: %d): %r",
            message.id,
            len(html_body),
            final_html_preview
        )

        # Create the email message object
        logger.info(
            "Creating AnymailMessage for message %s with metadata: agent_id=%s, attempt_id=%s",
            message.id,
            message.owner_agent_id,
            attempt.id
        )
        
        # Get CC addresses if any
        cc_addresses = []
        if message.cc_endpoints.exists():
            cc_addresses = list(message.cc_endpoints.values_list('address', flat=True))
            logger.info(
                "Email message %s includes CC recipients: %s",
                message.id,
                cc_addresses
            )
        
        msg = AnymailMessage(
            subject=subject,
            body=plaintext_body,
            from_email=from_header,
            to=[to_address],
            cc=cc_addresses if cc_addresses else None,
            connection=_get_postmark_connection(),
            tags=["persistent-agent"],
            headers={
                "Message-ID": message_rfc_id,
                **reply_headers,
            },
            metadata={
                "agent_id": str(message.owner_agent_id),
                "message_id": str(message.id),
                "attempt_id": str(attempt.id),
            },
        )

        attachment_count, rewritten_html_body = _attach_email_attachments(message, msg, html_body)
        html_body = rewritten_html_body
        if attachment_count:
            logger.info(
                "Attached %d file(s) to email message %s",
                attachment_count,
                message.id,
            )
        
        # Attach the HTML alternative
        logger.info("Attaching HTML alternative to message %s", message.id)
        msg.attach_alternative(html_body, "text/html")
        
        # Send the message
        logger.info(
            "Sending email message %s via Postmark. Final message summary: "
            "subject_length=%d, plaintext_length=%d, html_length=%d, to_recipients=%d",
            message.id,
            len(subject),
            len(plaintext_body),
            len(html_body),
            len([to_address])
        )
        
        msg.send(fail_silently=False)
        
        logger.info("Email message %s sent successfully via Postmark", message.id)

        span.add_event('Email - Postmark Delivery', {
            'message_id': str(message.id),
            'from_address': from_address,
            'to_address': to_address,
        })

        # On success, update records
        now = timezone.now()
        attempt.status = DeliveryStatus.SENT
        attempt.provider_message_id = msg.anymail_status.message_id or ""
        attempt.sent_at = now
        attempt.save()

        message.latest_status = DeliveryStatus.SENT
        message.latest_sent_at = now
        message.latest_error_message = ""
        message.save(update_fields=["latest_status", "latest_sent_at", "latest_error_message"])

        logger.info("Successfully sent agent email message %s via Postmark.", message.id)
        success_props = Analytics.with_org_properties(
            {
                'agent_id': str(message.owner_agent_id),
                'message_id': str(message.id),
                'from_address': from_address,
                'to_address': to_address,
                'subject': subject,
            },
            organization=getattr(message.owner_agent, "organization", None),
        )
        Analytics.track_event(
            user_id=message.owner_agent.user.id,
            event=AnalyticsEvent.PERSISTENT_AGENT_EMAIL_SENT,
            source=AnalyticsSource.AGENT,
            properties=success_props.copy(),
        )

    except AnymailAPIError as e:
        logger.exception(
            "Postmark API error sending message %s. Message details: from=%r, to=%r, subject=%r",
            message.id,
            message.from_endpoint.address,
            to_address,
            subject,
        )
        error_str = str(e)
        logger.error(
            "Email delivery failed for message %s with Postmark API error: %s",
            message.id,
            error_str
        )
        
        attempt.status = DeliveryStatus.FAILED
        attempt.error_message = error_str
        attempt.save()

        message.latest_status = DeliveryStatus.FAILED
        message.latest_error_message = error_str
        message.save(update_fields=["latest_status", "latest_error_message"])

    except Exception as e:
        logger.exception(
            "Unexpected error sending message %s. Message details: from=%r, to=%r, subject=%r",
            message.id,
            message.from_endpoint.address,
            to_address,
            subject,
        )
        error_str = f"An unexpected error occurred: {str(e)}"
        logger.error(
            "Email delivery failed for message %s with unexpected error: %s",
            message.id,
            error_str
        )
        
        attempt.status = DeliveryStatus.FAILED
        attempt.error_message = error_str
        attempt.save()

        message.latest_status = DeliveryStatus.FAILED
        message.latest_error_message = error_str
        message.save(update_fields=["latest_status", "latest_error_message"])


@tracer.start_as_current_span("AGENT - Deliver Agent SMS")
def deliver_agent_sms(message: PersistentAgentMessage):
    """Send an SMS and record the delivery attempt."""
    agent = getattr(message, "owner_agent", None)
    if getattr(agent, "sms_disabled", False):
        error_message = "SMS sending is disabled for this agent."
        logger.info(
            "Skipping SMS delivery for message %s because agent %s has SMS disabled.",
            message.id,
            getattr(agent, "id", None),
        )
        OutboundMessageAttempt.objects.create(
            message=message,
            provider="twilio",
            status=DeliveryStatus.FAILED,
            error_message=error_message,
        )
        message.latest_status = DeliveryStatus.FAILED
        message.latest_error_message = error_message
        message.save(update_fields=["latest_status", "latest_error_message"])
        return False

    # Mark the message as sending and record a new attempt
    message.latest_status = DeliveryStatus.SENDING
    message.save(update_fields=["latest_status"])

    attempt = OutboundMessageAttempt.objects.create(
        message=message,
        provider="twilio",
        status=DeliveryStatus.SENDING,
    )

    # Convert content to an SMS-friendly plaintext version
    original_body = message.body
    plaintext_body = _convert_sms_body_to_plaintext(original_body)

    try:
        from constants.feature_flags import AGENT_CRON_THROTTLE
        from config.redis_client import get_redis_client
        from api.services.cron_throttle import (
            build_upgrade_link,
            cron_throttle_footer_cooldown_key,
            cron_throttle_pending_footer_key,
            evaluate_free_plan_cron_throttle,
            select_cron_throttle_sms_suffix,
        )

        agent = getattr(message, "owner_agent", None)
        if agent is not None and switch_is_active(AGENT_CRON_THROTTLE):
            redis_client = get_redis_client()
            pending_key = cron_throttle_pending_footer_key(str(agent.id))
            if redis_client.get(pending_key):
                decision = evaluate_free_plan_cron_throttle(agent, (getattr(agent, "schedule", None) or ""))
                if decision.throttling_applies:
                    suffix = select_cron_throttle_sms_suffix(
                        agent_name=agent.name,
                        effective_interval_seconds=decision.effective_interval_seconds,
                        upgrade_link=build_upgrade_link(),
                    )
                    plaintext_body = f"{plaintext_body}\n\n{suffix}".strip()
                    ttl_days = int(getattr(settings, "AGENT_CRON_THROTTLE_NOTICE_TTL_DAYS", 7))
                    ttl_seconds = max(1, ttl_days * 86400)
                    redis_client.delete(pending_key)
                    redis_client.set(
                        cron_throttle_footer_cooldown_key(str(agent.id)),
                        "1",
                        ex=ttl_seconds,
                    )
                else:
                    redis_client.delete(pending_key)
    except Exception:
        logger.debug("Failed applying cron throttle SMS notice for message %s", message.id, exc_info=True)

    logger.info(
        "Prepared SMS body for message %s. Original length: %d, Plaintext length: %d",
        message.id,
        len(original_body),
        len(plaintext_body),
    )

    # Collect all recipient numbers (primary + CC for group messaging)
    recipient_numbers = [message.to_endpoint.address]
    if message.cc_endpoints.exists():
        cc_numbers = list(message.cc_endpoints.values_list('address', flat=True))
        recipient_numbers.extend(cc_numbers)
        logger.info(
            "SMS message %s is a group message with %d total recipients: %s",
            message.id,
            len(recipient_numbers),
            recipient_numbers
        )

    # Send to all recipients
    # Note: This sends individual messages to each recipient
    # For true group messaging, you'd need a different approach with your SMS provider
    send_results = []
    all_successful = True
    owner_user = getattr(getattr(message, "owner_agent", None), "user", None)
    
    for recipient in recipient_numbers:
        result = sms.send_sms(
            to_number=recipient,
            from_number=message.from_endpoint.address,
            body=plaintext_body,
            owner_user=owner_user,
        )
        send_results.append((recipient, result))
        if not result:
            all_successful = False
            logger.error(
                "Failed to send SMS to %s for message %s",
                recipient,
                message.id
            )
    
    send_result = all_successful

    now = timezone.now()

    if send_result:
        logger.info("Successfully sent agent SMS message %s via Twilio to all recipients.", message.id)
        # Store first successful message ID as the primary one
        provider_message_id = next((r[1] for r in send_results if r[1]), "")
        attempt.status = DeliveryStatus.SENT
        attempt.provider_message_id = provider_message_id
        attempt.sent_at = now
        attempt.save(update_fields=["status", "provider_message_id", "sent_at"])

        message.latest_status = DeliveryStatus.SENT
        message.latest_sent_at = now
        message.latest_error_message = ""

        sms_props = Analytics.with_org_properties(
            {
                "agent_id": str(message.owner_agent_id),
                "message_id": str(message.id),
                "sms_id": provider_message_id,
                "from_address": message.from_endpoint.address,
                "to_address": message.to_endpoint.address,
                "is_group": len(recipient_numbers) > 1,
                "recipient_count": len(recipient_numbers),
            },
            organization=getattr(message.owner_agent, "organization", None),
        )
        Analytics.track_event(
            user_id=message.owner_agent.user.id,
            event=AnalyticsEvent.PERSISTENT_AGENT_SMS_SENT,
            source=AnalyticsSource.AGENT,
            properties=sms_props.copy(),
        )
    else:
        logger.error("Failed to send agent SMS message %s via Twilio.", message.id)
        attempt.status = DeliveryStatus.FAILED
        attempt.error_message = "Failed to send SMS via Twilio."
        attempt.save(update_fields=["status", "error_message"])

        message.latest_status = DeliveryStatus.FAILED
        message.latest_error_message = "Failed to send SMS via Twilio."

        failure_props = Analytics.with_org_properties(
            {
                "agent_id": str(message.owner_agent_id),
                "message_id": str(message.id),
                "from_address": message.from_endpoint.address,
                "to_address": message.to_endpoint.address,
            },
            organization=getattr(message.owner_agent, "organization", None),
        )
        Analytics.track_event(
            user_id=message.owner_agent.user.id,
            event=AnalyticsEvent.PERSISTENT_AGENT_SMS_FAILED,
            source=AnalyticsSource.AGENT,
            properties=failure_props.copy(),
        )

    message.save(update_fields=["latest_status", "latest_sent_at", "latest_error_message"])

    return send_result
