"""complyform-onboarding-drip — daily customer lifecycle email automation.

Trigger: Daily Cloud Scheduler.

Three responsibilities per run:

A) Onboarding sequence for licenses < 14 days old — milestone state machine
   sends max 1 email/customer/day with 24hr minimum between sends.
B) NPS milestone surveys at day 30 and day 90.
C) License expiry warnings at 30 days and 7 days (skipped if cancellation pending).

Contract: community tier excluded.  Always returns 200 to Cloud Scheduler.
Writes customer_lifecycle/{customer_id} documents to Firestore.
"""

from __future__ import annotations

import hashlib
import hmac
import logging
from datetime import UTC, datetime, timedelta
from typing import Any

from backend.billing.email_sender import send_template_email
from backend.ops._shared import cloud_function_wrapper, log_action
from backend.shared.firestore import get_db

_log = logging.getLogger(__name__)

_NPS_BASE_URL = "https://complyform.dev/v1/nps"
_TOKEN_VALIDITY_DAYS = 30

# Milestone states (ordered progression).
_MILESTONE_STATES = (
    "activated",
    "nudge_sent",
    "first_scan",
    "remediate_explained",
    "day7_checkin",
    "day14_success",
    "completed",
)


# ---------------------------------------------------------------------------
# HMAC token helpers
# ---------------------------------------------------------------------------


async def _get_nps_signing_key() -> str:
    """Retrieve NPS signing key from Secret Manager."""
    from google.cloud import secretmanager

    client = secretmanager.SecretManagerServiceClient()
    name = "projects/complyform-prod/secrets/nps-signing-key/versions/latest"
    response = client.access_secret_version(request={"name": name})
    return response.payload.data.decode("utf-8")


def generate_nps_token(
    customer_id: str,
    token_type: str,
    expires_at: str,
    signing_key: str,
) -> str:
    """Generate HMAC-SHA256 signed token for NPS/cancellation feedback."""
    message = f"{customer_id}|{token_type}|{expires_at}"
    signature = hmac.new(
        signing_key.encode(),
        message.encode(),
        hashlib.sha256,
    ).hexdigest()
    return f"{customer_id}|{token_type}|{expires_at}|{signature}"


def build_feedback_url(
    customer_id: str,
    token_type: str,
    signing_key: str,
) -> str:
    """Build a signed feedback URL for email templates."""
    expires_at = (datetime.now(UTC) + timedelta(days=_TOKEN_VALIDITY_DAYS)).isoformat()
    token = generate_nps_token(customer_id, token_type, expires_at, signing_key)
    return f"{_NPS_BASE_URL}?token={token}"


# ---------------------------------------------------------------------------
# Telemetry detection
# ---------------------------------------------------------------------------


async def _has_scan_activity(
    db: Any,
    customer_id: str,
    since: datetime | None = None,
) -> bool:
    """Check if customer has scan activity (via projects subcollection)."""
    cust_doc = db.collection("customers").document(customer_id)
    projects_ref = cust_doc.collection("projects")
    async for project in projects_ref.stream():
        data = project.to_dict() or {}
        last_scan = data.get("last_scan")
        if last_scan is None:
            continue
        if since is None:
            return True
        if isinstance(last_scan, str):
            scan_dt = datetime.fromisoformat(last_scan)
        else:
            scan_dt = last_scan
        if scan_dt.tzinfo is None:
            scan_dt = scan_dt.replace(tzinfo=UTC)
        if scan_dt >= since:
            return True
    return False


async def _has_assess_activity(db: Any, customer_id: str) -> bool:
    """Check if customer has assessment activity."""
    cust_doc = db.collection("customers").document(customer_id)
    projects_ref = cust_doc.collection("projects")
    async for project in projects_ref.stream():
        data = project.to_dict() or {}
        if data.get("last_assess") is not None:
            return True
    return False


async def _has_remediate_activity(db: Any, customer_id: str) -> bool:
    """Check if customer has remediation activity."""
    cust_doc = db.collection("customers").document(customer_id)
    projects_ref = cust_doc.collection("projects")
    async for project in projects_ref.stream():
        data = project.to_dict() or {}
        if data.get("last_remediate") is not None:
            return True
    return False


# ---------------------------------------------------------------------------
# State machine transitions
# ---------------------------------------------------------------------------


async def _advance_onboarding(
    db: Any,
    customer_id: str,
    email: str,
    tier: str,
    activated_at: datetime,
    lifecycle: dict[str, Any],
    signing_key: str,
    now: datetime,
) -> dict[str, Any] | None:
    """Determine next milestone transition.  Returns updates dict or None."""
    state = lifecycle.get("milestone_state", "activated")
    days_since = (now - activated_at).days

    # Sequence terminates at day 14 regardless
    if days_since >= 14 and state != "completed" and state != "day14_success":
        has_scan = await _has_scan_activity(db, customer_id)
        has_assess = await _has_assess_activity(db, customer_id)
        if has_scan and has_assess:
            return await _send_milestone_email(
                email,
                "onboarding-day14-success",
                signing_key,
                customer_id,
                new_state="day14_success",
            )
        return {
            "milestone_state": "completed",
            "sequence_completed_at": now.isoformat(),
        }

    if state == "activated":
        # 24hr post-activation: nudge if no scan, else jump to first_scan
        if days_since >= 1:
            has_scan = await _has_scan_activity(db, customer_id)
            if has_scan:
                lifecycle_updates = await _send_milestone_email(
                    email,
                    "onboarding-activation-success",
                    signing_key,
                    customer_id,
                    new_state="first_scan",
                )
                if lifecycle_updates:
                    lifecycle_updates["first_scan_at"] = now.isoformat()
                return lifecycle_updates
            return await _send_milestone_email(
                email,
                "onboarding-first-scan-nudge",
                signing_key,
                customer_id,
                new_state="nudge_sent",
            )

    elif state == "nudge_sent":
        has_scan = await _has_scan_activity(db, customer_id)
        if has_scan:
            lifecycle_updates = await _send_milestone_email(
                email,
                "onboarding-activation-success",
                signing_key,
                customer_id,
                new_state="first_scan",
            )
            if lifecycle_updates:
                lifecycle_updates["first_scan_at"] = now.isoformat()
            return lifecycle_updates

    elif state == "first_scan":
        # 3d post-scan, no remediate
        first_scan_at_str = lifecycle.get("first_scan_at")
        if first_scan_at_str:
            first_scan_at = datetime.fromisoformat(first_scan_at_str)
            if first_scan_at.tzinfo is None:
                first_scan_at = first_scan_at.replace(tzinfo=UTC)
            if (now - first_scan_at).days >= 3:
                has_remediate = await _has_remediate_activity(db, customer_id)
                if not has_remediate:
                    return await _send_milestone_email(
                        email,
                        "onboarding-remediate-explainer",
                        signing_key,
                        customer_id,
                        new_state="remediate_explained",
                    )
                else:
                    lifecycle_updates: dict[str, Any] = {
                        "milestone_state": "remediate_explained",
                        "first_remediate_at": now.isoformat(),
                    }
                    return lifecycle_updates

    elif state == "remediate_explained":
        if days_since >= 7:
            has_recent_scan = await _has_scan_activity(
                db,
                customer_id,
                since=now - timedelta(days=5),
            )
            if not has_recent_scan:
                return await _send_milestone_email(
                    email,
                    "onboarding-day7-checkin",
                    signing_key,
                    customer_id,
                    new_state="day7_checkin",
                )
            # Active — skip day7_checkin
            return {"milestone_state": "day7_checkin"}

    elif state == "day7_checkin":
        if days_since >= 14:
            has_scan = await _has_scan_activity(db, customer_id)
            has_assess = await _has_assess_activity(db, customer_id)
            if has_scan and has_assess:
                return await _send_milestone_email(
                    email,
                    "onboarding-day14-success",
                    signing_key,
                    customer_id,
                    new_state="day14_success",
                )
            return {
                "milestone_state": "completed",
                "sequence_completed_at": now.isoformat(),
            }

    elif state == "day14_success":
        return {
            "milestone_state": "completed",
            "sequence_completed_at": now.isoformat(),
        }

    return None


async def _send_milestone_email(
    email: str,
    template_alias: str,
    signing_key: str,
    customer_id: str,
    *,
    new_state: str,
) -> dict[str, Any] | None:
    """Send milestone email and return lifecycle update fields."""
    ok = await send_template_email(
        to=email,
        template_alias=template_alias,
        template_model={"customer_id": customer_id},
    )
    if not ok:
        _log.warning("Failed to send %s to %s", template_alias, email)
    return {
        "milestone_state": new_state,
        "last_email_sent": template_alias,
        "last_email_at": datetime.now(UTC).isoformat(),
    }


# ---------------------------------------------------------------------------
# NPS milestones
# ---------------------------------------------------------------------------


async def _check_nps_milestones(
    db: Any,
    customer_id: str,
    email: str,
    activated_at: datetime,
    lifecycle: dict[str, Any],
    signing_key: str,
    now: datetime,
) -> dict[str, Any]:
    """Send NPS survey at day 30 and day 90.  Returns update fields."""
    days_since = (now - activated_at).days
    updates: dict[str, Any] = {}

    if days_since >= 30 and lifecycle.get("nps_30_sent_at") is None:
        url = build_feedback_url(customer_id, "nps", signing_key)
        ok = await send_template_email(
            to=email,
            template_alias="nps-survey",
            template_model={"feedback_url": url, "customer_id": customer_id},
        )
        if ok:
            updates["nps_30_sent_at"] = now.isoformat()

    if days_since >= 90 and lifecycle.get("nps_90_sent_at") is None:
        url = build_feedback_url(customer_id, "nps", signing_key)
        ok = await send_template_email(
            to=email,
            template_alias="nps-survey",
            template_model={"feedback_url": url, "customer_id": customer_id},
        )
        if ok:
            updates["nps_90_sent_at"] = now.isoformat()

    return updates


# ---------------------------------------------------------------------------
# Expiry warnings
# ---------------------------------------------------------------------------


async def _check_expiry_warnings(
    customer_id: str,
    email: str,
    customer_data: dict[str, Any],
    lifecycle: dict[str, Any],
    now: datetime,
) -> dict[str, Any]:
    """Send license expiry warnings at 30d and 7d.  Returns update fields."""
    if customer_data.get("cancellation_pending"):
        return {}

    expires_at_str = customer_data.get("subscription_expires_at")
    if not expires_at_str:
        return {}

    expires_at = datetime.fromisoformat(expires_at_str)
    if expires_at.tzinfo is None:
        expires_at = expires_at.replace(tzinfo=UTC)

    days_remaining = (expires_at - now).days
    updates: dict[str, Any] = {}

    if days_remaining <= 30 and lifecycle.get("expiry_30d_sent_at") is None:
        ok = await send_template_email(
            to=email,
            template_alias="license-expiry-warning",
            template_model={"days_remaining": 30, "customer_id": customer_id},
        )
        if ok:
            updates["expiry_30d_sent_at"] = now.isoformat()

    if days_remaining <= 7 and lifecycle.get("expiry_7d_sent_at") is None:
        ok = await send_template_email(
            to=email,
            template_alias="license-expiry-warning",
            template_model={"days_remaining": 7, "customer_id": customer_id},
        )
        if ok:
            updates["expiry_7d_sent_at"] = now.isoformat()

    return updates


# ---------------------------------------------------------------------------
# Main entry point
# ---------------------------------------------------------------------------


@cloud_function_wrapper("complyform-onboarding-drip")
async def onboarding_drip(request: Any) -> dict[str, Any]:
    """Customer lifecycle email automation.

    Three responsibilities in one daily run:

    A) ONBOARDING SEQUENCE (licenses < 14 days old):
       Milestone state machine with max 1 email/customer/day.
    B) NPS MILESTONES (all active licenses):
       Day 30 and day 90 NPS surveys.
    C) LICENSE EXPIRY WARNINGS (all active non-cancelled licenses):
       30d and 7d warnings (skipped if cancellation_pending).
    """
    db = get_db()
    now = datetime.now(UTC)
    # Use 16 days to ensure day-14 terminal transitions are processed
    # even with sub-second timing differences between function start and
    # license creation timestamps.
    cutoff_14d = now - timedelta(days=16)

    try:
        signing_key = await _get_nps_signing_key()
    except Exception:
        _log.exception("Failed to get NPS signing key — aborting")
        return {"emails_sent": 0, "error": "signing_key_unavailable"}

    emails_sent = 0
    at_risk_count = 0

    # Query all active license keys
    lk_ref = db.collection("license_keys")
    query = lk_ref.where("status", "==", "active")

    async for doc in query.stream():
        data = doc.to_dict() or {}
        customer_id = data.get("customer_id", "")
        email_addr = data.get("email", "")
        tier = data.get("tier", "")
        created_at_str = data.get("created_at", "")

        if not customer_id or not email_addr:
            continue

        # Community tier excluded
        if tier == "community":
            continue

        if not created_at_str:
            continue

        activated_at = datetime.fromisoformat(created_at_str)
        if activated_at.tzinfo is None:
            activated_at = activated_at.replace(tzinfo=UTC)

        # Load or initialize lifecycle document
        lifecycle_ref = db.collection("customer_lifecycle").document(customer_id)
        lifecycle_snap = await lifecycle_ref.get()
        lifecycle: dict[str, Any] = (
            lifecycle_snap.to_dict() if lifecycle_snap.exists else {}
        ) or {}

        updates: dict[str, Any] = {
            "customer_id": customer_id,
            "email": email_addr,
            "tier": tier,
            "activated_at": activated_at.isoformat(),
        }

        # Check 24hr email gap
        last_email_at_str = lifecycle.get("last_email_at")
        can_send_email = True
        if last_email_at_str:
            last_email_at = datetime.fromisoformat(last_email_at_str)
            if last_email_at.tzinfo is None:
                last_email_at = last_email_at.replace(tzinfo=UTC)
            if (now - last_email_at) < timedelta(hours=24):
                can_send_email = False

        # A) Onboarding sequence (< 14 days old)
        if activated_at >= cutoff_14d and can_send_email:
            milestone_state = lifecycle.get("milestone_state", "activated")
            if milestone_state != "completed":
                milestone_updates = await _advance_onboarding(
                    db,
                    customer_id,
                    email_addr,
                    tier,
                    activated_at,
                    lifecycle,
                    signing_key,
                    now,
                )
                if milestone_updates:
                    updates.update(milestone_updates)
                    if "last_email_sent" in milestone_updates:
                        emails_sent += 1

        # Activation-at-risk detection
        days_since = (now - activated_at).days
        if days_since >= 7:
            has_any_scan = await _has_scan_activity(db, customer_id)
            if not has_any_scan:
                updates["at_risk"] = True
                at_risk_count += 1
            else:
                updates["at_risk"] = False

        # B) NPS milestones
        nps_updates = await _check_nps_milestones(
            db,
            customer_id,
            email_addr,
            activated_at,
            lifecycle,
            signing_key,
            now,
        )
        if nps_updates:
            updates.update(nps_updates)
            emails_sent += len(nps_updates)

        # C) Expiry warnings — need customer data for cancellation_pending
        customer_snap = await db.collection("customers").document(customer_id).get()
        customer_data: dict[str, Any] = {}
        if customer_snap.exists:
            customer_data = customer_snap.to_dict() or {}

        expiry_updates = await _check_expiry_warnings(
            customer_id,
            email_addr,
            customer_data,
            lifecycle,
            now,
        )
        if expiry_updates:
            updates.update(expiry_updates)
            emails_sent += len(expiry_updates)

        # Write lifecycle document
        await lifecycle_ref.set(updates, merge=True)

    log_action(
        customer_id=None,
        action="onboarding_drip_summary",
        emails_sent=emails_sent,
        at_risk_count=at_risk_count,
    )

    return {"emails_sent": emails_sent, "at_risk_count": at_risk_count}


entry_point = onboarding_drip
