"""
Trial drip email service.

Scans active trials on a cron schedule, sends drip emails at hard-coded
day offsets, idempotent on (account_id, template_id) for non-test sends.
"""

import hashlib
import hmac
import logging
import os
import re
from datetime import datetime, timezone, timedelta
from html import escape as html_escape
from typing import Optional

from sqlalchemy import select, update as sa_update
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession

from ..db import AsyncSessionLocal
from ..models import (
    Account,
    AccountEntitlements,
    User,
    TrialEmailTemplate,
    TrialEmailSend,
)

log = logging.getLogger(__name__)

DRIP_SCHEDULE = [0, 2, 7, 23, 27, 29]
SEND_WINDOW_HOURS = 4
MAX_SENDS_PER_SCAN = 50

SETUP_LINK = "https://app.foundryops.io/agent/quickstart"
BOOTSTRAP_PROMPT = (
    "Install g-gremlin, authenticate with my trial token, connect my Salesforce, "
    "and run g-gremlin doctor to verify everything works"
)
CAN_SPAM_ADDRESS = "FoundryOps, Inc."

# Secret for HMAC unsubscribe tokens — falls back to a dev value.
_UNSUB_SECRET = os.getenv("TRIAL_UNSUB_SECRET", "dev-unsub-secret-change-me")


# ── Unsubscribe tokens ──────────────────────────────────────────────────────

def generate_unsubscribe_token(account_id: str) -> str:
    """Create an HMAC-SHA256 token for stateless unsubscribe links."""
    return hmac.new(
        _UNSUB_SECRET.encode(), account_id.encode(), hashlib.sha256
    ).hexdigest()


def verify_unsubscribe_token(account_id: str, token: str) -> bool:
    return hmac.compare_digest(
        token, generate_unsubscribe_token(account_id)
    )


# ── Template rendering ───────────────────────────────────────────────────────

def _render_vars(
    text: str,
    *,
    first_name: str,
    org_name: str,
    trial_end_date: str,
    unsubscribe_link: str,
) -> str:
    """Replace {{var}} placeholders in template text."""
    return (
        text.replace("{{first_name}}", first_name)
        .replace("{{org_name}}", org_name)
        .replace("{{trial_end_date}}", trial_end_date)
        .replace("{{setup_link}}", SETUP_LINK)
        .replace("{{bootstrap_prompt}}", BOOTSTRAP_PROMPT)
        .replace("{{unsubscribe_link}}", unsubscribe_link)
    )


_SIMPLE_MD_PATTERNS = [
    # bold
    (re.compile(r"\*\*(.+?)\*\*"), r"<strong>\1</strong>"),
    # links
    (re.compile(r"\[([^\]]+)\]\(([^)]+)\)"), r'<a href="\2" style="color:#f97316;text-decoration:none;">\1</a>'),
    # code blocks
    (re.compile(r"```\n?(.*?)\n?```", re.S), r'<pre style="background:#f4f4f5;padding:12px;border-radius:6px;font-size:13px;overflow-x:auto;">\1</pre>'),
    # inline code
    (re.compile(r"`([^`]+)`"), r'<code style="background:#f4f4f5;padding:2px 6px;border-radius:4px;font-size:13px;">\1</code>'),
    # blockquotes
    (re.compile(r"^>\s?(.+)$", re.M), r'<blockquote style="border-left:3px solid #f97316;padding-left:12px;margin:12px 0;color:#6b7280;">\1</blockquote>'),
    # numbered list items
    (re.compile(r"^\d+\.\s+(.+)$", re.M), r"<li>\1</li>"),
    # bullet list items
    (re.compile(r"^[-*]\s+(.+)$", re.M), r"<li>\1</li>"),
    # horizontal rule
    (re.compile(r"^---$", re.M), r'<hr style="border:none;border-top:1px solid #e5e7eb;margin:16px 0;">'),
]


_CTA_RE = re.compile(
    r'^<p style="margin:0 0 16px 0;">'
    r'<a href="([^"]+)" style="[^"]*">([^<]+)</a>'
    r"</p>$"
)


def _md_to_html(md: str) -> str:
    """Minimal markdown-to-HTML converter (no external dependency)."""
    html = md  # use raw md — templates are admin-authored
    for pattern, repl in _SIMPLE_MD_PATTERNS:
        html = pattern.sub(repl, html)
    # Paragraphs: double newlines
    paragraphs = re.split(r"\n{2,}", html)
    parts = []
    for p in paragraphs:
        stripped = p.strip()
        if not stripped:
            continue
        if stripped.startswith("<"):
            parts.append(stripped)
        else:
            parts.append(f'<p style="margin:0 0 16px 0;">{stripped}</p>')
    html = "".join(parts)
    # Convert standalone link paragraphs into CTA buttons
    html = _CTA_RE.sub(
        r'<table cellpadding="0" cellspacing="0" style="margin:24px 0;">'
        r'<tr><td style="background-color:#f97316;border-radius:8px;">'
        r'<a href="\1" style="display:inline-block;padding:12px 28px;'
        r"color:#ffffff;font-size:15px;font-weight:600;text-decoration:none;"
        r'letter-spacing:-0.2px;">\2</a>'
        r"</td></tr></table>",
        html,
    )
    return html


def _md_to_plain(md: str) -> str:
    """Strip markdown formatting for plain-text email body."""
    text = md
    text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
    text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1 (\2)", text)
    text = re.sub(r"```\n?(.*?)\n?```", r"\1", text, flags=re.S)
    text = re.sub(r"`([^`]+)`", r"\1", text)
    text = re.sub(r"^>\s?", "", text, flags=re.M)
    return text


LOGO_URL = "https://storage.googleapis.com/g-gremlin-releases/static/logo-hex.png"


def _wrap_branded_html(inner_html: str, unsubscribe_link: str) -> str:
    """Wrap rendered content in the branded FoundryOps email shell."""
    return f"""<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->
</head>
<body style="margin:0;padding:0;background-color:#0a0a0a;font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
  <table width="100%" cellpadding="0" cellspacing="0" style="background-color:#0a0a0a;padding:40px 20px;">
    <tr>
      <td align="center">
        <!-- Header with logo -->
        <table width="600" cellpadding="0" cellspacing="0" style="background-color:#0a0a0a;">
          <tr>
            <td style="padding:32px 40px 24px;text-align:left;">
              <table cellpadding="0" cellspacing="0">
                <tr>
                  <td style="vertical-align:middle;padding-right:12px;">
                    <img src="{LOGO_URL}" alt="FoundryOps" width="36" height="36" style="display:block;border:0;outline:none;border-radius:8px;" />
                  </td>
                  <td style="vertical-align:middle;">
                    <span style="color:#ededed;font-size:18px;font-weight:700;letter-spacing:-0.3px;">FoundryOps</span>
                  </td>
                </tr>
              </table>
            </td>
          </tr>
        </table>

        <!-- Body card -->
        <table width="600" cellpadding="0" cellspacing="0" style="background-color:#ffffff;border-radius:12px;overflow:hidden;">
          <tr>
            <td style="padding:40px;color:#374151;font-size:15px;line-height:1.7;">
              {inner_html}
            </td>
          </tr>
        </table>

        <!-- Footer -->
        <table width="600" cellpadding="0" cellspacing="0" style="background-color:#0a0a0a;">
          <tr>
            <td style="padding:24px 40px;text-align:center;">
              <p style="color:#6b7280;font-size:12px;margin:0;">
                {CAN_SPAM_ADDRESS}
              </p>
              <p style="color:#4b5563;font-size:11px;margin:8px 0 0 0;">
                <a href="{unsubscribe_link}" style="color:#4b5563;text-decoration:underline;">Unsubscribe</a>
              </p>
            </td>
          </tr>
        </table>
      </td>
    </tr>
  </table>
</body>
</html>"""


def render_email(
    template: TrialEmailTemplate,
    *,
    first_name: str,
    org_name: str,
    trial_end_date: str,
    account_id: str,
) -> tuple[str, str, str]:
    """Return (subject, text_body, html_body) for a drip template."""
    token = generate_unsubscribe_token(account_id)
    base_url = os.getenv("FM_BASE_URL", "https://app.foundryops.io")
    unsubscribe_link = (
        f"{base_url}/api/v2/admin/trial-emails/unsubscribe"
        f"?a={account_id}&t={token}"
    )

    subject = _render_vars(
        template.subject,
        first_name=first_name,
        org_name=org_name,
        trial_end_date=trial_end_date,
        unsubscribe_link=unsubscribe_link,
    )

    body_md = _render_vars(
        template.body_markdown,
        first_name=first_name,
        org_name=org_name,
        trial_end_date=trial_end_date,
        unsubscribe_link=unsubscribe_link,
    )

    text_body = _md_to_plain(body_md)
    inner_html = _md_to_html(body_md)
    html_body = _wrap_branded_html(inner_html, unsubscribe_link)

    return subject, text_body, html_body


# ── Send logic ───────────────────────────────────────────────────────────────

async def send_drip_email(
    db: AsyncSession,
    template: TrialEmailTemplate,
    account: Account,
    user: User,
    trial_end_date: datetime,
) -> Optional[str]:
    """Send one drip email. Returns resend_email_id or None on skip/error."""
    from .email_notifications import _send_via_resend_with_id

    if account.do_not_contact:
        log.debug("Skipping drip for %s — do_not_contact", account.id)
        return None

    first_name = _extract_first_name(user)

    subject, text_body, html_body = render_email(
        template,
        first_name=first_name,
        org_name=account.name or "your organization",
        trial_end_date=trial_end_date.strftime("%B %d, %Y"),
        account_id=account.id,
    )

    # Idempotent insert — skip if row already exists for non-test sends.
    stmt = (
        pg_insert(TrialEmailSend)
        .values(
            template_id=template.id,
            account_id=account.id,
            user_id=user.id,
            recipient_email=user.email,
            day_offset=template.day_offset,
            status="pending",
            is_test=False,
        )
        .on_conflict_do_nothing(
            index_elements=["account_id", "template_id"],
            index_where=TrialEmailSend.is_test == False,  # noqa: E712
        )
        .returning(TrialEmailSend.id)
    )
    result = await db.execute(stmt)
    send_id = result.scalar_one_or_none()
    if send_id is None:
        log.debug(
            "Drip already sent: account=%s template=%s", account.id, template.slug
        )
        return None

    # Actually send via Resend.
    try:
        resend_id = await _send_via_resend_with_id(
            user.email,
            subject,
            text_body,
            html_body=html_body,
            tags=[
                {"name": "message_type", "value": "trial_drip"},
                {"name": "day_offset", "value": str(template.day_offset)},
                {"name": "account_id", "value": account.id},
            ],
        )
        status = "sent" if resend_id else "error"
        await db.execute(
            sa_update(TrialEmailSend)
            .where(TrialEmailSend.id == send_id)
            .values(resend_email_id=resend_id, status=status)
        )
        await db.commit()
        log.info(
            "Drip sent: account=%s day=%s resend_id=%s",
            account.id,
            template.day_offset,
            resend_id,
        )
        return resend_id
    except Exception as exc:
        log.error("Drip send failed: account=%s err=%s", account.id, exc)
        await db.execute(
            sa_update(TrialEmailSend)
            .where(TrialEmailSend.id == send_id)
            .values(status="error", error_message=str(exc)[:500])
        )
        await db.commit()
        return None


def _extract_first_name(user: User) -> str:
    """Best-effort first name from user metadata or email."""
    if hasattr(user, "profile_metadata") and isinstance(user.profile_metadata, dict):
        name = user.profile_metadata.get("first_name") or user.profile_metadata.get(
            "name", ""
        )
        if name:
            return name.split()[0]
    # Fallback: part before @ in email
    return user.email.split("@")[0].split(".")[0].title()


# ── Cron scan ────────────────────────────────────────────────────────────────

async def run_trial_drip_scan() -> None:
    """
    Cron entry point: scan active trials and send due drip emails.

    Called every 4 hours by APScheduler.  The send window ensures each
    email is only eligible during a 4-hour window after its due time,
    preventing stale email blasts on first deploy or after downtime.
    """
    if os.getenv("TRIAL_DRIP_ENABLED", "true").lower() in ("false", "0", "no"):
        log.info("[DRIP] Kill switch active (TRIAL_DRIP_ENABLED=false), skipping")
        return

    log.info("[DRIP] Starting trial drip scan")
    async with AsyncSessionLocal() as db:
        # 1. Load enabled templates
        tpl_result = await db.execute(
            select(TrialEmailTemplate).where(
                TrialEmailTemplate.enabled == True,  # noqa: E712
                TrialEmailTemplate.day_offset >= 0,  # exclude manual templates
            )
        )
        templates = {t.day_offset: t for t in tpl_result.scalars().all()}
        if not templates:
            log.info("[DRIP] No enabled templates, exiting")
            return

        # 2. Find active trial accounts (skip paid upgrades)
        now = datetime.now(timezone.utc)
        cutoff = now - timedelta(days=1)
        ent_result = await db.execute(
            select(AccountEntitlements).where(
                AccountEntitlements.trial_started_at.isnot(None),
                AccountEntitlements.expires_at.isnot(None),
                AccountEntitlements.expires_at > cutoff,
                AccountEntitlements.tier == "free",  # stop drips once they upgrade
            )
        )
        entitlements = ent_result.scalars().all()
        log.info("[DRIP] Found %d active free-tier trials", len(entitlements))

        sent_count = 0
        for ent in entitlements:
            if sent_count >= MAX_SENDS_PER_SCAN:
                log.info("[DRIP] Hit send cap (%d), stopping", MAX_SENDS_PER_SCAN)
                break

            trial_start = ent.trial_started_at
            if trial_start is None or ent.expires_at is None:
                continue

            for day_offset, template in templates.items():
                if sent_count >= MAX_SENDS_PER_SCAN:
                    break

                due_at = trial_start + timedelta(days=day_offset)
                window_end = due_at + timedelta(hours=SEND_WINDOW_HOURS)

                if not (due_at <= now < window_end):
                    continue

                # Fetch account + primary user
                acc = await db.get(Account, ent.account_id)
                if not acc:
                    continue

                user_result = await db.execute(
                    select(User)
                    .where(User.account_id == ent.account_id)
                    .order_by(User.created_at)
                    .limit(1)
                )
                user = user_result.scalar_one_or_none()
                if not user:
                    continue

                resend_id = await send_drip_email(
                    db, template, acc, user, ent.expires_at
                )
                if resend_id:
                    sent_count += 1

        log.info("[DRIP] Scan complete, sent %d emails", sent_count)
