import os
import requests
import logging
from typing import Any, List, Optional, Sequence, Tuple, Union

from django.core.exceptions import ValidationError
from django.core.validators import validate_email

logger = logging.getLogger(__name__)

MAILGUN_COMPANY_NAME = os.getenv('MAILGUN_COMPANY_NAME')
ENABLE_MAILGUN = (os.getenv('ENABLE_MAILGUN') or "").lower() == "true"
LOG_MAILGUN_RESPONSE_TEXT = (os.getenv('MAILGUN_LOG_RESPONSE_TEXT') or "").lower() == "true"
MAILGUN_API_KEY = os.getenv('MAILGUN_API_KEY')
MAILGUN_DOMAIN = os.getenv('MAILGUN_DOMAIN')
# Comma-separated list, for example: "audit@example.com,ops@example.com"
# Default is intentionally empty to avoid accidental data leakage.
BCC_RECIPIENTS = os.getenv('MAILGUN_BCC_RECIPIENTS', "")


RecipientInput = Union[str, Sequence[str], None]
Attachment = Tuple[str, Any, str]
EnabledOverride = Union[bool, str, None]


def _recipients_to_list(value: RecipientInput) -> List[str]:
    """Convert a recipient input into a normalized list of email strings.

    Accepts:
    - None/empty: returns []
    - iterable (list/tuple/set): strips each item
    - str: splits on commas and strips each part

    This is primarily used for merging static recipients (e.g. `BCC_RECIPIENTS`)
    with recipients passed into `send_mailgun_email`.

    In the BCC merge flow, the final recipient list merges + trims + de-dupes
    (case-insensitive) while preserving order.
    """
    if not value:
        return []
    if isinstance(value, (list, tuple, set)):
        return [str(v).strip() for v in value if str(v).strip()]
    # Allow comma-separated strings
    return [part.strip() for part in str(value).split(",") if part.strip()]


def _filter_valid_emails(emails: Sequence[str], field_name: str) -> List[str]:
    # Runtime safety: validate each email and drop invalids instead of letting
    # Mailgun reject the whole request.
    valid: List[str] = []
    for email in emails:
        candidate = str(email).strip()
        if not candidate:
            continue
        try:
            validate_email(candidate)
            valid.append(candidate)
        except ValidationError:
            logger.warning("Invalid email dropped for %s: %r", field_name, candidate)
    return valid


def _format_like_original(original: Any, emails: Sequence[str]) -> Union[str, List[str]]:
    # Preserve caller intent:
    # - if they passed a string, send a comma-separated string
    # - if they passed an iterable, send a list (requests encodes repeated fields)
    if isinstance(original, str):
        return ", ".join(emails)
    return list(emails)


def _truncate_for_debug(value: str, max_chars: int = 500) -> str:
    if len(value) <= max_chars:
        return value
    return f"{value[:max_chars]}...<truncated>"


def _parse_enabled_override(enabled: EnabledOverride) -> Optional[bool]:
    if enabled is None:
        return None
    if isinstance(enabled, bool):
        return enabled
    if isinstance(enabled, str):
        normalized = enabled.strip().lower()
        if normalized in {"1", "true", "yes", "y", "on"}:
            return True
        if normalized in {"0", "false", "no", "n", "off", ""}:
            return False
    raise ValueError(
        "enabled must be a bool or one of: true/false/1/0/yes/no/on/off"
    )


def send_mailgun_email(
    to: Union[str, Sequence[str]],
    subject: str,
    html: str,
    attachments: Optional[Sequence[Attachment]] = None,
    cc: RecipientInput = None,
    bcc: RecipientInput = None,
    enabled: EnabledOverride = None,
) -> Optional[requests.Response]:
    """Send an email using Mailgun.

    Args:
        to: Recipient email(s). Accepts a string (single or comma-separated) or an iterable of emails.
        subject: Email subject.
        html: HTML body.
        attachments: Optional list of tuples: (filename, file_bytes_or_fileobj, mimetype).
        cc: Optional CC recipient(s). Accepts a string (single or comma-separated) or an iterable of emails.
        bcc: Optional additional BCC recipient(s). Accepts a string (single or comma-separated) or an iterable.
            Note: `BCC_RECIPIENTS` (if configured) are always included when sending.
        enabled: Optional runtime override for the enable flag.
            - If None, uses `ENABLE_MAILGUN` from environment.
            - If provided, accepts bool or common truthy/falsey strings and is
              used as source of truth.

    Examples:
        # Single recipient as a string
        send_mailgun_email(
            to="user@example.com",
            subject="Hello",
            html="<p>Hi</p>",
        )

        # Multiple recipients as a comma-separated string
        send_mailgun_email(
            to="user1@example.com, user2@example.com",
            subject="Hello all",
            html="<p>Hi</p>",
        )

        # Multiple recipients as a list (this codebase commonly uses this form)
        send_mailgun_email(
            to=["user1@example.com", "user2@example.com"],
            subject="Hello all",
            html="<p>Hi</p>",
        )

        # CC/BCC can be a string or a list as well
        send_mailgun_email(
            to="user@example.com",
            subject="Hello",
            html="<p>Hi</p>",
            cc="manager1@example.com, manager2@example.com",
            bcc=["audit@example.com"],
        )

        # With an attachment
        send_mailgun_email(
            to="user@example.com",
            subject="Report",
            html="<p>Attached</p>",
            attachments=[("report.pdf", pdf_bytes, "application/pdf")],
        )
    """
    try:
        parsed_enabled = _parse_enabled_override(enabled)
    except ValueError:
        logger.error("Mailgun email not sent: invalid enabled override: %r", enabled)
        return None

    mailgun_enabled = ENABLE_MAILGUN if parsed_enabled is None else parsed_enabled

    if mailgun_enabled:
        missing_config: List[str] = []
        if not (MAILGUN_DOMAIN or "").strip():
            missing_config.append("MAILGUN_DOMAIN")
        if not (MAILGUN_API_KEY or "").strip():
            missing_config.append("MAILGUN_API_KEY")
        if not (MAILGUN_COMPANY_NAME or "").strip():
            missing_config.append("MAILGUN_COMPANY_NAME")

        if missing_config:
            logger.error(
                "Mailgun email not sent: missing required configuration: %s",
                ", ".join(missing_config),
            )
            return None

        url = f"https://api.mailgun.net/v3/{MAILGUN_DOMAIN}/messages"

        # Normalize + validate recipients; abort if we can't produce a valid "to".
        to_list = _filter_valid_emails(_recipients_to_list(to), field_name="to")
        if not to_list:
            logger.warning("Mailgun email not sent: no valid 'to' recipients")
            return None
        to_normalized = _format_like_original(to, to_list)
        data = {
            "from": MAILGUN_COMPANY_NAME,
            "to": to_normalized,
            "subject": subject,
            "html": html,
        }

        # Optional CC: only include if at least one valid recipient remains.
        if cc is not None:
            cc_list = _filter_valid_emails(_recipients_to_list(cc), field_name="cc")
            if cc_list:
                data["cc"] = _format_like_original(cc, cc_list)

        # BCC always includes static recipients plus any passed-in bcc.
        static_bcc = _recipients_to_list(BCC_RECIPIENTS)
        passed_bcc = _recipients_to_list(bcc)
        merged_bcc = _filter_valid_emails(static_bcc + passed_bcc, field_name="bcc")
        if merged_bcc:
            # De-dupe case-insensitively while preserving order.
            seen = set()
            deduped_bcc: List[str] = []
            for recipient in merged_bcc:
                key = recipient.lower()
                if key in seen:
                    continue
                seen.add(key)
                deduped_bcc.append(recipient)
            data["bcc"] = deduped_bcc

        if attachments:
            files = []
            for attachment in attachments:
                files.append(("attachment", (attachment[0], attachment[1], attachment[2])))
        else:
            files = None

        try:
            response = requests.post(
                url,
                auth=("api", MAILGUN_API_KEY),
                data=data,
                files=files,
                timeout=10
            )
            logger.info("Mailgun response: response_code=%s", response.status_code)
            if LOG_MAILGUN_RESPONSE_TEXT and logger.isEnabledFor(logging.DEBUG):
                logger.debug(
                    "Mailgun response text preview: %s",
                    _truncate_for_debug(response.text or ""),
                )
            return response
        except requests.RequestException:
            logger.exception("Mailgun request failed")
            return None
    else:
        logger.info("Mailgun is disabled. Email not sent.")
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug(
                "Mail metadata: to_count=%d cc_count=%d bcc_count=%d attachments_count=%d",
                len(_recipients_to_list(to)),
                len(_recipients_to_list(cc)),
                len(_recipients_to_list(bcc)),
                len(attachments or []),
            )
        return None