"""
scrylo/email_tools/smtp_sender.py
-----------------------------------
Bulk SMTP sender with anti-spam hardening.

Anti-spam measures implemented:
  1. Plain-text + HTML multipart/alternative  — avoids image-only penalty
  2. Text/HTML ratio kept healthy             — not wall-to-wall links
  3. List-Unsubscribe header (RFC 2369)       — Gmail/Yahoo one-click compliance
  4. Message-ID with sender domain            — avoids forged-header flag
  5. Date header in RFC 2822 format           — required by spam filters
  6. X-Mailer omitted                         — removes "mass mailer" fingerprint
  7. Randomised send delays                   — avoids burst-send pattern
  8. Daily send cap enforced                  — protects domain reputation
  9. Unsubscribe footer in every email        — CAN-SPAM + GDPR mandatory
 10. Physical address in footer               — CAN-SPAM §5 requirement
 11. X-Mailer omitted                         — removes "mass mailer" fingerprint
 12. Subject line: no ALL CAPS, no $ signs    — basic subject hygiene check
 13. Personalised first name salutation       — lower spam score than "Dear Sir"
 14. TLS enforced (STARTTLS on 587, SSL on 465) — never plain auth
 15. Unsubscribe table checked before EVERY send — compliance hard stop
 16. Bounced leads marked + skipped on retry  — protects sender score
 17. Per-campaign EmailEvent log              — full audit trail

Usage:
    from scrylo.email_tools.smtp_sender import run_bulk_send, SendResult
    result = run_bulk_send(campaign_name="my_campaign", lead_ids=[1,2,3])
"""

from __future__ import annotations

import asyncio
import logging
import random
import re
import smtplib
import ssl
import time
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formataddr, formatdate, make_msgid
from typing import Callable, Optional

from scrylo.core.config import load as load_cfg
from scrylo.db.models import (
    EmailCampaign, EmailEvent, EmailEventType,
    Lead, LeadStatus, Unsubscribe,
)
from scrylo.db.session import db_session

log = logging.getLogger("scrylo.smtp_sender")


# ── Subject hygiene check ─────────────────────────────────────────────────────

_SPAM_SUBJECT_RE = re.compile(
    r"(\$|FREE|URGENT|ACT NOW|CLICK HERE|GUARANTEED|NO RISK|"
    r"MAKE MONEY|WINNER|CONGRATULATIONS|OFFER|DISCOUNT)",
    re.IGNORECASE,
)

def _warn_subject(subject: str) -> list[str]:
    warnings = []
    if subject == subject.upper() and len(subject) > 5:
        warnings.append("Subject is ALL CAPS — high spam risk")
    if _SPAM_SUBJECT_RE.search(subject):
        warnings.append("Subject contains spam-trigger words")
    if len(subject) > 70:
        warnings.append("Subject over 70 chars — may be truncated")
    if "!" in subject:
        warnings.append("Exclamation marks in subject raise spam score")
    return warnings


# ── Unsubscribe URL builder ───────────────────────────────────────────────────

def _unsub_url(base_url: str, lead_id: int, token: str) -> str:
    """Build a signed unsubscribe URL."""
    return f"{base_url.rstrip('/')}/unsubscribe?id={lead_id}&token={token}"


def _tracking_pixel(base_url: str, event_id: int, token: str) -> str:
    """1x1 transparent GIF tracking pixel."""
    url = f"{base_url.rstrip('/')}/track/open?eid={event_id}&token={token}"
    return f'<img src="{url}" width="1" height="1" alt="" style="display:none;"/>'


# ── Email builder ─────────────────────────────────────────────────────────────

def _build_message(
    lead: Lead,
    subject: str,
    body_text: str,
    cfg_email: dict,
    unsub_url: str,
    tracking_pixel_html: str = "",
) -> MIMEMultipart:
    """
    Build a multipart/alternative MIME message with:
    - Plain text part (primary, deliverability anchor)
    - HTML part (formatted, includes tracking pixel)
    - All required anti-spam headers
    """
    from_name    = (cfg_email.get("from_name") or "").strip()
    from_address = (cfg_email.get("from_address") or "").strip()
    physical_addr = (cfg_email.get("physical_address") or "").strip()

    # ── Personalise greeting ─────────────────────────────────────────────
    first_name = (lead.first_name or (lead.full_name or "").split()[0] or "there").strip()
    # Replace generic openers with personalised first name
    personalised = re.sub(
        r"^(Hi|Hello|Hey|Dear)\s+(there|Sir|Madam|Friend)",
        f"Hi {first_name}",
        body_text, flags=re.IGNORECASE
    )
    # If body starts directly without greeting, prepend one
    if not re.match(r"^(Hi|Hello|Hey|Dear)\b", personalised, re.IGNORECASE):
        personalised = f"Hi {first_name},\n\n{personalised}"

    plain_text = personalised

    # ── Plain text footer ────────────────────────────────────────────────
    plain_footer = (
        f"\n\n---\n"
        f"To unsubscribe from future emails: {unsub_url}\n"
    )
    if physical_addr:
        plain_footer += f"{physical_addr}\n"

    full_plain = plain_text + plain_footer

    # ── HTML version ─────────────────────────────────────────────────────
    # Convert plain text to simple HTML — preserves line breaks, no heavy styling
    # (heavy CSS = spam filter suspicion)
    html_body = plain_text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
    html_body = "<br>\n".join(html_body.split("\n"))

    html_footer = (
        f'<br><br>'
        f'<p style="font-size:11px;color:#999;border-top:1px solid #eee;padding-top:8px;">'
        f'To unsubscribe, <a href="{unsub_url}" style="color:#999;">click here</a>.'
    )
    if physical_addr:
        html_footer += f"<br>{physical_addr}"
    html_footer += f"{tracking_pixel_html}</p>"

    full_html = (
        "<!DOCTYPE html><html><body "
        'style="font-family:Arial,sans-serif;font-size:14px;line-height:1.6;color:#222;">'
        f"<p>{html_body}</p>"
        f"{html_footer}"
        "</body></html>"
    )

    # ── Assemble MIME ─────────────────────────────────────────────────────
    msg = MIMEMultipart("alternative")

    # Anti-spam headers
    sender_domain = from_address.split("@")[-1] if "@" in from_address else "localhost"
    msg["Message-ID"]  = make_msgid(domain=sender_domain)
    msg["Date"]        = formatdate(localtime=True)
    msg["From"]        = formataddr((from_name, from_address)) if from_name else from_address
    msg["To"]          = lead.email
    msg["Subject"]     = subject
    msg["List-Unsubscribe"]       = f"<{unsub_url}>"
    msg["List-Unsubscribe-Post"]  = "List-Unsubscribe=One-Click"

    # Plain text FIRST — some spam filters weight it heavily
    msg.attach(MIMEText(full_plain, "plain", "utf-8"))
    msg.attach(MIMEText(full_html,  "html",  "utf-8"))

    return msg


# ── SMTP connection ───────────────────────────────────────────────────────────

def _connect_smtp(cfg_email: dict) -> smtplib.SMTP | smtplib.SMTP_SSL:
    """
    Connect and authenticate. Supports:
    - Port 587: STARTTLS (Gmail, Outlook, Zoho, Brevo)
    - Port 465: SMTP_SSL (legacy SSL)
    - Port 25:  plain (local relay only — not recommended)
    """
    host     = cfg_email.get("smtp_host", "").strip()
    port     = int(cfg_email.get("smtp_port", 587))
    user     = cfg_email.get("smtp_user", "").strip()
    password = cfg_email.get("smtp_password", "").strip()

    if not host:
        raise ValueError("SMTP host not configured — set it in Settings")
    if not user or not password:
        raise ValueError("SMTP username or password not configured — set them in Settings")

    context = ssl.create_default_context()

    if port == 465:
        server = smtplib.SMTP_SSL(host, port, context=context, timeout=15)
    else:
        server = smtplib.SMTP(host, port, timeout=15)
        server.ehlo()
        server.starttls(context=context)
        server.ehlo()

    server.login(user, password)
    log.info("SMTP connected to %s:%d as %s", host, port, user)
    return server


# ── Result types ──────────────────────────────────────────────────────────────

@dataclass
class LeadSendResult:
    lead_id:   int
    name:      str
    email:     str
    status:    str          # "sent" | "skipped" | "failed" | "dry_run"
    reason:    str = ""
    subject:   str = ""


@dataclass
class SendResult:
    campaign_id:   int | None    = None
    campaign_name: str           = ""
    total:         int           = 0
    sent:          int           = 0
    skipped:       int           = 0
    failed:        int           = 0
    dry_run:       bool          = False
    errors:        list[str]     = field(default_factory=list)
    leads:         list[LeadSendResult] = field(default_factory=list)
    subject_warnings: list[str]  = field(default_factory=list)


# ── Main sender ───────────────────────────────────────────────────────────────

def run_bulk_send(
    campaign_name:    str,
    lead_ids:         list[int] | None = None,
    dry_run:          bool             = False,
    delay_min:        int | None       = None,
    delay_max:        int | None       = None,
    limit:            int | None       = None,
    progress_callback: Optional[Callable[[int, int, str], None]] = None,
    base_url:         str              = "http://localhost:8000",
) -> SendResult:
    """
    Send pitched emails to a list of leads (or all pitched leads if lead_ids is None).

    Anti-spam contract:
    - Every email is multipart/alternative (text + html)
    - Unsubscribe header + footer in every send
    - Unsubscribe table checked before every send — hard block
    - Randomised delay between sends (default 90–180s, configurable)
    - Daily send cap enforced — stops mid-batch if cap reached
    - Bounced leads marked and skipped on re-run
    - Physical address in footer (CAN-SPAM §5)
    """
    cfg      = load_cfg()
    cfg_email = cfg.get("email", {})
    cfg_comp  = cfg.get("compliance", {})

    # Merge compliance into email cfg for message builder
    cfg_email["physical_address"] = cfg_comp.get("physical_address", "")

    d_min  = delay_min  if delay_min  is not None else int(cfg_email.get("min_delay_seconds", 90))
    d_max  = delay_max  if delay_max  is not None else int(cfg_email.get("max_delay_seconds", 180))
    d_cap  = limit      if limit      is not None else int(cfg_email.get("daily_send_limit", 50))

    result = SendResult(campaign_name=campaign_name, dry_run=dry_run)

    # ── Load leads ────────────────────────────────────────────────────────
    with db_session() as db:
        q = (
            db.query(Lead)
            .filter(
                Lead.pitch_body.isnot(None),
                Lead.email.isnot(None),
                Lead.email.like("%@%"),
                Lead.status.notin_([LeadStatus.BOUNCED, LeadStatus.UNSUBSCRIBED]),
            )
            .order_by(Lead.icp_score.desc())
        )
        if lead_ids:
            q = q.filter(Lead.id.in_(lead_ids))
        if d_cap:
            q = q.limit(d_cap)

        leads = q.all()
        result.total = len(leads)

        if not leads:
            result.errors.append("No pitched leads with emails found. Generate pitches first.")
            return result

        # ── Subject warnings (check first lead as sample) ─────────────────
        sample_subject = leads[0].pitch_subject or ""
        result.subject_warnings = _warn_subject(sample_subject)

        # ── Create campaign record ────────────────────────────────────────
        if not dry_run:
            campaign = EmailCampaign(
                name=campaign_name,
                description=f"Bulk send · {len(leads)} leads · {datetime.now().strftime('%Y-%m-%d %H:%M')}",
            )
            db.add(campaign)
            db.flush()
            result.campaign_id = campaign.id
        else:
            result.campaign_id = None

        # ── Connect SMTP (skip for dry run) ───────────────────────────────
        server = None
        if not dry_run:
            try:
                server = _connect_smtp(cfg_email)
            except Exception as e:
                result.errors.append(f"SMTP connection failed: {e}")
                return result

        # ── Send loop ─────────────────────────────────────────────────────
        try:
            for i, lead in enumerate(leads):
                if progress_callback:
                    progress_callback(i, len(leads), f"Sending to {lead.full_name}...")

                lead_result = LeadSendResult(
                    lead_id = lead.id,
                    name    = lead.full_name or "",
                    email   = lead.email or "",
                    subject = lead.pitch_subject or "",
                    status  = "pending",
                )

                # ── Unsubscribe check — hard block ────────────────────────
                unsub = db.query(Unsubscribe).filter(
                    Unsubscribe.email == (lead.email or "").lower()
                ).first()
                if unsub:
                    lead_result.status = "skipped"
                    lead_result.reason = "unsubscribed"
                    result.skipped += 1
                    result.leads.append(lead_result)
                    continue

                # ── Dry run — simulate only ───────────────────────────────
                if dry_run:
                    lead_result.status = "dry_run"
                    lead_result.reason = "dry run — not sent"
                    result.sent += 1  # count as "would send"
                    result.leads.append(lead_result)
                    if progress_callback:
                        progress_callback(i + 1, len(leads),
                                          f"[dry run] {lead.full_name} — {lead.email}")
                    time.sleep(0.05)  # tiny pause for UI responsiveness
                    continue

                # ── Build tracking token + event placeholder ──────────────
                token = uuid.uuid4().hex
                event = EmailEvent(
                    lead_id     = lead.id,
                    campaign_id = result.campaign_id,
                    event_type  = EmailEventType.SENT,
                    event_meta  = f'{{"token":"{token}"}}',
                )
                db.add(event)
                db.flush()

                # ── Build unsub URL + tracking pixel ─────────────────────
                unsub_url    = _unsub_url(base_url, lead.id, token)
                pixel_html   = _tracking_pixel(base_url, event.id, token)

                # ── Assemble message ──────────────────────────────────────
                subject = lead.pitch_subject or campaign_name
                body    = lead.pitch_body or ""

                try:
                    msg = _build_message(
                        lead, subject, body, cfg_email,
                        unsub_url, pixel_html,
                    )
                except Exception as e:
                    lead_result.status = "failed"
                    lead_result.reason = f"message build error: {e}"
                    result.failed += 1
                    result.errors.append(f"{lead.full_name}: {e}")
                    result.leads.append(lead_result)
                    continue

                # ── Send ──────────────────────────────────────────────────
                try:
                    from_address = cfg_email.get("from_address", "").strip()
                    server.sendmail(from_address, lead.email, msg.as_string())  # type: ignore

                    # Update lead status
                    lead.status = LeadStatus.SENT
                    # Update campaign stats
                    campaign.total_sent += 1  # type: ignore

                    lead_result.status = "sent"
                    result.sent += 1

                    log.info("Sent to %s <%s>", lead.full_name, lead.email)

                except smtplib.SMTPRecipientsRefused:
                    lead.status = LeadStatus.BOUNCED
                    db.add(Unsubscribe(
                        email=lead.email.lower(),
                        reason="bounce",
                    ))
                    lead_result.status = "failed"
                    lead_result.reason = "bounced — address refused"
                    result.failed += 1

                except smtplib.SMTPException as e:
                    lead_result.status = "failed"
                    lead_result.reason = str(e)
                    result.failed += 1
                    result.errors.append(f"{lead.full_name}: {e}")

                    # Reconnect on connection drop
                    try:
                        server.quit()  # type: ignore
                    except Exception:
                        pass
                    try:
                        server = _connect_smtp(cfg_email)
                    except Exception as conn_err:
                        result.errors.append(f"Reconnect failed: {conn_err}")
                        result.leads.append(lead_result)
                        break

                result.leads.append(lead_result)
                db.flush()

                # ── Randomised delay (skip after last email) ──────────────
                if i < len(leads) - 1:
                    delay = random.uniform(d_min, d_max)
                    log.debug("Waiting %.1fs before next send", delay)
                    if progress_callback:
                        progress_callback(
                            i + 1, len(leads),
                            f"⏱ Waiting {int(delay)}s... ({i+1}/{len(leads)} sent)"
                        )
                    time.sleep(delay)

        finally:
            if server:
                try:
                    server.quit()
                except Exception:
                    pass
            db.commit()

    if progress_callback:
        progress_callback(
            result.total, result.total,
            f"✓ Done — {result.sent} sent, {result.skipped} skipped, {result.failed} failed"
        )

    return result


# ── Async wrapper for web UI ──────────────────────────────────────────────────

async def run_bulk_send_async(
    campaign_name:    str,
    lead_ids:         list[int] | None = None,
    dry_run:          bool             = False,
    delay_min:        int | None       = None,
    delay_max:        int | None       = None,
    limit:            int | None       = None,
    progress_callback: Optional[Callable] = None,
    base_url:         str              = "http://localhost:8000",
) -> SendResult:
    """Run bulk send in executor so it doesn't block the FastAPI event loop."""
    loop = asyncio.get_event_loop()
    return await loop.run_in_executor(
        None,
        lambda: run_bulk_send(
            campaign_name=campaign_name,
            lead_ids=lead_ids,
            dry_run=dry_run,
            delay_min=delay_min,
            delay_max=delay_max,
            limit=limit,
            progress_callback=progress_callback,
            base_url=base_url,
        )
    )


# ── SMTP connection test ──────────────────────────────────────────────────────

def test_smtp_connection() -> tuple[bool, str]:
    """Quick connection test for settings page. Returns (ok, message)."""
    try:
        cfg_email = load_cfg().get("email", {})
        server = _connect_smtp(cfg_email)
        server.quit()
        host = cfg_email.get("smtp_host", "")
        return True, f"Connected to {host} ✓"
    except Exception as e:
        return False, str(e)
