"""Paddle billing webhook handler — core event processing logic.

Handles four Paddle event types:
- ``transaction.completed`` (checkout + renewal)
- ``transaction.past_due``
- ``subscription.updated``
- ``subscription.canceled``

Every handler is idempotent: re-processing the same ``event_id``
returns immediately with no side effects.
"""

from __future__ import annotations

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

from backend.billing.email_sender import send_template_email
from backend.billing.license_generator import generate_api_key, generate_license_key
from backend.shared.firestore import (
    create_api_key,
    create_customer,
    create_license_key,
    find_license_key_by_customer,
    get_customer_by_subscription,
    is_event_processed,
    mark_event_processed,
    update_customer,
    update_license_key,
)
from backend.shared.region_mapping import resolve_data_region
from backend.shared.tier_config import ALL_FRAMEWORKS, TIER_CONFIG, TIER_RANK

_log = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Price ID → Tier mapping
# ---------------------------------------------------------------------------

PRICE_TO_TIER: dict[str, str] = {
    "pri_pro_annual": "pro",
    "pri_team_annual": "team",
    "pri_agency_base_annual": "agency",
    "pri_enterprise_base_annual": "enterprise",
    "pri_enterprise_unlimited_annual": "enterprise_unlimited",
}

OVERAGE_PRICES: frozenset[str] = frozenset(
    [
        "pri_agency_overage",
        "pri_enterprise_overage",
    ]
)

_DOCS_URL = "https://complyform.dev/docs"
_DASHBOARD_URL = "https://dashboard.complyform.dev"
_PADDLE_PORTAL_URL = "https://complyform.dev/billing"
_FEEDBACK_URL = "https://complyform.dev/feedback"


# ---------------------------------------------------------------------------
# Signature verification
# ---------------------------------------------------------------------------


def verify_paddle_signature(
    raw_body: bytes,
    signature_header: str,
    secret: str,
) -> bool:
    """Verify Paddle webhook signature.

    ``Paddle-Signature`` format: ``ts=<timestamp>;h1=<hmac_sha256_hex>``

    Rejects if timestamp differs from now by more than 300 seconds.
    """
    parts: dict[str, str] = {}
    for segment in signature_header.split(";"):
        if "=" in segment:
            k, v = segment.split("=", 1)
            parts[k] = v

    ts = parts.get("ts", "")
    h1 = parts.get("h1", "")

    if not ts or not h1:
        return False

    try:
        ts_int = int(ts)
    except ValueError:
        return False

    if abs(time.time() - ts_int) > 300:
        return False

    message = f"{ts}:{raw_body.decode('utf-8')}"
    computed = hmac.new(
        secret.encode("utf-8"),
        message.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(computed, h1)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _resolve_frameworks(tier_config_entry: dict[str, object]) -> list[str]:
    """Resolve ``["all"]`` to the full framework list."""
    raw = tier_config_entry.get("remediate_frameworks", [])
    if not isinstance(raw, list):
        return []
    if raw == ["all"]:
        return sorted(ALL_FRAMEWORKS)
    return [str(f) for f in raw]


def _resolve_features(tier_config_entry: dict[str, object]) -> list[str]:
    raw = tier_config_entry.get("features", [])
    return [str(f) for f in raw] if isinstance(raw, list) else []


def _has_feature(tier: str, feature: str) -> bool:
    cfg = TIER_CONFIG.get(tier, {})
    features = cfg.get("features", [])
    return isinstance(features, list) and feature in features


def _available_regions(tier: str) -> list[str]:
    cfg = TIER_CONFIG.get(tier, {})
    regions = cfg.get("available_regions")
    if isinstance(regions, list):
        return [str(r) for r in regions]
    return ["us"]


def _extract_price_id(event_data: dict[str, Any]) -> str | None:
    """Extract the first non-overage price ID from transaction items."""
    items = event_data.get("items", [])
    for item in items:
        price = item.get("price", {})
        pid = price.get("id", "")
        if pid in PRICE_TO_TIER:
            return pid
    return None


def _has_overage_item(event_data: dict[str, Any]) -> bool:
    """Check if any transaction item is an overage charge."""
    items = event_data.get("items", [])
    return any(item.get("price", {}).get("id", "") in OVERAGE_PRICES for item in items)


# ---------------------------------------------------------------------------
# Event dispatcher
# ---------------------------------------------------------------------------


async def handle_webhook_event(event: dict[str, Any]) -> None:
    """Dispatch a verified Paddle webhook event to the correct handler.

    Idempotent: skips already-processed events.
    """
    event_id: str = event.get("event_id", "")
    event_type: str = event.get("event_type", "")

    if not event_id or not event_type:
        _log.warning("Webhook missing event_id or event_type, skipping")
        return

    if await is_event_processed(event_id):
        _log.info("Event %s already processed, skipping", event_id)
        return

    data: dict[str, Any] = event.get("data", {})

    try:
        if event_type == "transaction.completed":
            await _handle_transaction_completed(data)
        elif event_type == "transaction.past_due":
            await _handle_transaction_past_due(data)
        elif event_type == "subscription.updated":
            await _handle_subscription_updated(data)
        elif event_type == "subscription.canceled":
            await _handle_subscription_canceled(data)
        else:
            _log.info("Unhandled event type: %s", event_type)
    except Exception:
        _log.exception("Error processing %s event %s", event_type, event_id)

    await mark_event_processed(event_id, event_type)


# ---------------------------------------------------------------------------
# transaction.completed
# ---------------------------------------------------------------------------


async def _handle_transaction_completed(data: dict[str, Any]) -> None:
    subscription_id: str = data.get("subscription_id", "")
    customer_paddle_id: str = data.get("customer_id", "")
    email: str = data.get("customer", {}).get("email", "") or data.get(
        "checkout", {}
    ).get("customer", {}).get("email", "")

    if not subscription_id:
        _log.warning("transaction.completed missing subscription_id")
        return

    # Distinguish checkout vs renewal
    existing = await get_customer_by_subscription(subscription_id)
    if existing is not None:
        await _handle_renewal(existing[0], existing[1])
        return

    # --- New checkout ---
    price_id = _extract_price_id(data)
    if price_id is None:
        _log.warning("transaction.completed: no recognizable price ID")
        return

    tier = PRICE_TO_TIER[price_id]
    tier_cfg = TIER_CONFIG[tier]

    license_key, license_key_hash = generate_license_key()

    api_key: str | None = None
    api_key_hash: str | None = None
    if _has_feature(tier, "hosted_dashboard"):
        api_key, api_key_hash = generate_api_key()

    # Resolve data region
    address = data.get("address", {}) or {}
    country_code = address.get("country_code")
    regions = _available_regions(tier)
    data_region = resolve_data_region(country_code, regions)

    now_iso = datetime.now(UTC).isoformat()
    expires_at = (datetime.now(UTC) + timedelta(days=365)).isoformat()

    # Use customer_paddle_id as doc ID (ctm_xxx)
    customer_id = customer_paddle_id

    # Extract email from multiple possible locations
    if not email:
        billing_details = data.get("billing_details", {}) or {}
        email = billing_details.get("email", "")

    customer_doc: dict[str, Any] = {
        "email": email,
        "tier": tier,
        "paddle_subscription_id": subscription_id,
        "paddle_customer_id": customer_paddle_id,
        "license_key_hash": license_key_hash,
        "api_key_hash": api_key_hash,
        "org_id": None,
        "status": "active",
        "previous_tier": None,
        "cancellation_pending": False,
        "current_tier_started_at": now_iso,
        "delivery_method": "online",
        "data_region": data_region,
        "data_region_set_at": now_iso,
        "project_limit": tier_cfg.get("project_limit"),
        "batch_limit": tier_cfg.get("batch_limit"),
        "retention_months": tier_cfg.get("retention_months"),
        "created_at": now_iso,
        "cancelled_at": None,
        "subscription_expires_at": expires_at,
        "overage_past_due_since": None,
        "email_delivery_failed": False,
    }
    await create_customer(customer_id, customer_doc)

    # License key doc
    frameworks = _resolve_frameworks(tier_cfg)
    features = _resolve_features(tier_cfg)
    await create_license_key(
        license_key_hash,
        {
            "customer_id": customer_id,
            "tier": tier,
            "frameworks": frameworks,
            "features": features,
            "status": "active",
            "created_at": now_iso,
            "expires_at": expires_at,
        },
    )

    # API key doc
    if api_key_hash is not None:
        await create_api_key(
            api_key_hash,
            {
                "customer_id": customer_id,
                "tier": tier,
                "created_at": now_iso,
            },
        )

    # Record consent from custom_data
    custom_data = data.get("custom_data") or {}
    tos_accepted_at = custom_data.get("tos_accepted_at", now_iso)
    tos_version = custom_data.get("tos_version", "1.0")
    privacy_version = custom_data.get("privacy_version", "1.0")

    try:
        from backend.shared.firestore import record_consent

        await record_consent(
            customer_id,
            tos_accepted_at=tos_accepted_at,
            tos_version=tos_version,
            privacy_accepted_at=tos_accepted_at,
            privacy_version=privacy_version,
            consent_source="paddle_checkout",
        )
    except Exception:
        _log.exception("Failed to record consent for %s", customer_id)

    # Send email
    activation_cmd = f"complyform config activate {license_key}"

    if api_key is not None:
        email_ok = await send_template_email(
            to=email,
            template_alias="license-key-delivery-team",
            template_model={
                "license_key": license_key,
                "api_key": api_key,
                "tier": tier,
                "activation_command": activation_cmd,
                "dashboard_url": _DASHBOARD_URL,
                "docs_url": _DOCS_URL,
            },
        )
    else:
        email_ok = await send_template_email(
            to=email,
            template_alias="license-key-delivery",
            template_model={
                "license_key": license_key,
                "tier": tier,
                "activation_command": activation_cmd,
                "docs_url": _DOCS_URL,
            },
        )

    if not email_ok:
        _log.error("Email delivery failed for customer %s", customer_id)
        await update_customer(customer_id, {"email_delivery_failed": True})


async def _handle_renewal(customer_id: str, customer_data: dict[str, Any]) -> None:
    """Extend subscription by 1 year on renewal (silent — no email)."""
    current_expiry_str = customer_data.get("subscription_expires_at", "")
    try:
        current_expiry = datetime.fromisoformat(current_expiry_str)
    except ValueError, TypeError:
        current_expiry = datetime.now(UTC)

    new_expiry = (current_expiry + timedelta(days=365)).isoformat()

    await update_customer(customer_id, {"subscription_expires_at": new_expiry})

    # Update license key expiry
    lk = await find_license_key_by_customer(customer_id)
    if lk is not None:
        key_hash, _ = lk
        await update_license_key(key_hash, {"expires_at": new_expiry})

    _log.info("Renewed subscription for %s, new expiry %s", customer_id, new_expiry)


# ---------------------------------------------------------------------------
# transaction.past_due
# ---------------------------------------------------------------------------


async def _handle_transaction_past_due(data: dict[str, Any]) -> None:
    subscription_id: str = data.get("subscription_id", "")
    if not subscription_id:
        _log.warning("transaction.past_due missing subscription_id")
        return

    result = await get_customer_by_subscription(subscription_id)
    if result is None:
        _log.warning("transaction.past_due: no customer for sub %s", subscription_id)
        return

    customer_id, customer_data = result

    if not _has_overage_item(data):
        # Subscription past-due — Paddle Retain handles dunning
        _log.info("Subscription past-due for %s, Paddle handles dunning", customer_id)
        return

    # Overage past-due — first occurrence only
    if customer_data.get("overage_past_due_since") is not None:
        _log.info("Overage already flagged for %s, skipping", customer_id)
        return

    now_iso = datetime.now(UTC).isoformat()
    deadline = (datetime.now(UTC) + timedelta(days=30)).strftime("%Y-%m-%d")

    await update_customer(customer_id, {"overage_past_due_since": now_iso})

    tier = customer_data.get("tier", "unknown")
    amount_due = ""
    items = data.get("items", [])
    for item in items:
        if item.get("price", {}).get("id", "") in OVERAGE_PRICES:
            totals = item.get("totals", {})
            amount_due = totals.get("total", "")
            break

    email = customer_data.get("email", "")
    if email:
        email_ok = await send_template_email(
            to=email,
            template_alias="overage-payment-warning",
            template_model={
                "tier": tier,
                "amount_due": str(amount_due),
                "paddle_portal_url": _PADDLE_PORTAL_URL,
                "deadline_date": deadline,
            },
        )
        if not email_ok:
            _log.error("Overage warning email failed for %s", customer_id)
            await update_customer(customer_id, {"email_delivery_failed": True})


# ---------------------------------------------------------------------------
# subscription.updated
# ---------------------------------------------------------------------------


async def _handle_subscription_updated(data: dict[str, Any]) -> None:
    subscription_id: str = data.get("id", "") or data.get("subscription_id", "")
    if not subscription_id:
        _log.warning("subscription.updated missing subscription ID")
        return

    # Check for billing-relevant changes
    previous_attrs = data.get("previous_attributes", {}) or {}
    if "items" not in previous_attrs:
        _log.info("subscription.updated: no item changes for %s", subscription_id)
        return

    result = await get_customer_by_subscription(subscription_id)
    if result is None:
        _log.warning("subscription.updated: no customer for sub %s", subscription_id)
        return

    customer_id, customer_data = result

    # Resolve old and new tiers
    old_tier = customer_data.get("tier", "community")

    new_price_id = _extract_price_id(data)
    if new_price_id is None:
        _log.warning(
            "subscription.updated: cannot determine new price for %s", subscription_id
        )
        return

    new_tier = PRICE_TO_TIER.get(new_price_id)
    if new_tier is None:
        _log.warning("subscription.updated: unrecognized price %s", new_price_id)
        return

    # Defense-in-depth: reject downgrades
    if TIER_RANK.get(new_tier, 0) <= TIER_RANK.get(old_tier, 0):
        _log.warning(
            "Rejected downgrade %s -> %s for customer %s",
            old_tier,
            new_tier,
            customer_id,
        )
        return

    # Valid upgrade
    new_cfg = TIER_CONFIG[new_tier]
    now_iso = datetime.now(UTC).isoformat()
    frameworks = _resolve_frameworks(new_cfg)
    features = _resolve_features(new_cfg)

    await update_customer(
        customer_id,
        {
            "tier": new_tier,
            "previous_tier": old_tier,
            "current_tier_started_at": now_iso,
            "project_limit": new_cfg.get("project_limit"),
            "batch_limit": new_cfg.get("batch_limit"),
            "retention_months": new_cfg.get("retention_months"),
        },
    )

    # Update license key
    lk = await find_license_key_by_customer(customer_id)
    if lk is not None:
        key_hash, _ = lk
        await update_license_key(
            key_hash,
            {
                "tier": new_tier,
                "frameworks": frameworks,
                "features": features,
            },
        )

    # Check dashboard boundary crossing
    old_has_dashboard = _has_feature(old_tier, "hosted_dashboard")
    new_has_dashboard = _has_feature(new_tier, "hosted_dashboard")
    api_key: str | None = None

    if not old_has_dashboard and new_has_dashboard:
        api_key, api_key_hash = generate_api_key()
        await create_api_key(
            api_key_hash,
            {
                "customer_id": customer_id,
                "tier": new_tier,
                "created_at": now_iso,
            },
        )
        await update_customer(customer_id, {"api_key_hash": api_key_hash})

    # Send upgrade confirmation
    email = customer_data.get("email", "")
    if email:
        template_model: dict[str, Any] = {
            "old_tier": old_tier,
            "new_tier": new_tier,
            "activation_command": "complyform config activate <your-license-key>",
        }
        if not old_has_dashboard and new_has_dashboard and api_key is not None:
            template_model["dashboard_url"] = _DASHBOARD_URL
            template_model["api_key"] = api_key

        await send_template_email(
            to=email,
            template_alias="tier-upgrade-confirmation",
            template_model=template_model,
        )

    _log.info("Upgraded %s from %s to %s", customer_id, old_tier, new_tier)


# ---------------------------------------------------------------------------
# subscription.canceled
# ---------------------------------------------------------------------------


async def _handle_subscription_canceled(data: dict[str, Any]) -> None:
    subscription_id: str = data.get("id", "") or data.get("subscription_id", "")
    if not subscription_id:
        _log.warning("subscription.canceled missing subscription ID")
        return

    result = await get_customer_by_subscription(subscription_id)
    if result is None:
        _log.warning("subscription.canceled: no customer for sub %s", subscription_id)
        return

    customer_id, customer_data = result
    now_iso = datetime.now(UTC).isoformat()

    await update_customer(
        customer_id,
        {
            "status": "cancelled",
            "cancelled_at": now_iso,
        },
    )

    # Revoke license key
    lk = await find_license_key_by_customer(customer_id)
    if lk is not None:
        key_hash, _ = lk
        await update_license_key(key_hash, {"status": "revoked"})

    # Send cancellation feedback email with signed NPS token
    email = customer_data.get("email", "")
    tier = customer_data.get("tier", "unknown")
    if email:
        try:
            from backend.ops.onboarding_drip import (
                _get_nps_signing_key,
                build_feedback_url,
            )

            signing_key = await _get_nps_signing_key()
            feedback_url = build_feedback_url(customer_id, "cancellation", signing_key)
        except Exception:
            _log.warning("Failed to generate signed feedback URL, using fallback")
            feedback_url = _FEEDBACK_URL
        await send_template_email(
            to=email,
            template_alias="cancellation-feedback",
            template_model={
                "tier": tier,
                "feedback_url": feedback_url,
            },
        )

    _log.info("Cancelled subscription for %s", customer_id)
