"""complyform-usage-reconciler — monthly overage billing via Paddle one-time charges.

Trigger: Daily Cloud Scheduler (06:00 UTC — after dormancy on Sundays).

Bills Agency/Enterprise customers for active projects exceeding the base 5
limit.  Enterprise Unlimited is exempt.  Idempotent via ``last_overage_billed_at``
guard (>30 days between charges).

Contract: per-customer error isolation.  HTTP 4xx from Paddle is logged and
the customer is skipped (retried on next daily run).
"""

from __future__ import annotations

import logging
import os
from datetime import UTC, datetime, timedelta
from typing import Any

import httpx

from backend.ops._shared import cloud_function_wrapper, log_action
from backend.shared.firestore import get_db
from backend.shared.tier_config import TIER_CONFIG

_log = logging.getLogger(__name__)

_PADDLE_API_BASE = "https://api.paddle.com"
_BILLING_PERIOD_DAYS = 30

# Project limits per tier.
PROJECT_LIMITS: dict[str, int | None] = {
    tier: (
        cfg.get("project_limit")  # type: ignore[misc]
        if isinstance(cfg.get("project_limit"), int)
        else None
    )
    for tier, cfg in TIER_CONFIG.items()
}

# Tiers eligible for overage billing.
_OVERAGE_TIERS: frozenset[str] = frozenset(["agency", "enterprise"])

# Overage price IDs from environment.
_OVERAGE_PRICE_IDS: dict[str, str] = {
    "agency": os.environ.get("PADDLE_PRICE_AGENCY_OVERAGE", "pri_agency_overage"),
    "enterprise": os.environ.get(
        "PADDLE_PRICE_ENTERPRISE_OVERAGE", "pri_enterprise_overage"
    ),
}


async def _get_paddle_api_key() -> str:
    """Retrieve Paddle API key from Secret Manager."""
    from google.cloud import secretmanager

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


@cloud_function_wrapper("complyform-usage-reconciler")
async def usage_reconciler(request: Any) -> dict[str, Any]:
    """Monthly overage billing for Agency/Enterprise via Paddle one-time charges."""
    db = get_db()
    now = datetime.now(UTC)
    stats: dict[str, int] = {"customers_billed": 0, "total_overage": 0}

    paddle_key = await _get_paddle_api_key()

    query = db.collection("customers").where("status", "==", "active")
    async for doc in query.stream():
        customer_id = doc.id
        try:
            data = doc.to_dict() or {}
            tier = data.get("tier", "")
            if tier not in _OVERAGE_TIERS:
                continue

            limit = PROJECT_LIMITS.get(tier)
            if limit is None:
                continue

            subscription_id = data.get("paddle_subscription_id")
            if not subscription_id:
                continue

            # Idempotency: skip if billed within the last 30 days
            last_billed = data.get("last_overage_billed_at")
            if last_billed:
                try:
                    last_dt = datetime.fromisoformat(last_billed)
                    if (now - last_dt) < timedelta(days=_BILLING_PERIOD_DAYS):
                        continue
                except ValueError, TypeError:
                    pass

            # Count active projects (exclude archived, suspended, dormant)
            active_count = await _count_active_projects(db, customer_id)
            if active_count <= limit:
                continue

            overage_count = active_count - limit
            price_id = _OVERAGE_PRICE_IDS.get(tier)
            if not price_id:
                continue

            # Bill via Paddle
            success = await _charge_overage(
                paddle_key, subscription_id, price_id, overage_count
            )
            if success:
                await (
                    db.collection("customers")
                    .document(customer_id)
                    .update({"last_overage_billed_at": now.isoformat()})
                )
                stats["customers_billed"] += 1
                stats["total_overage"] += overage_count
                log_action(
                    customer_id,
                    "overage_billed",
                    record_count=overage_count,
                    tier=tier,
                )
            else:
                log_action(
                    customer_id,
                    "overage_billing_failed",
                    error="Paddle API error",
                    tier=tier,
                )
        except Exception as exc:
            log_action(customer_id, "overage_billing_error", error=str(exc))
            _log.exception("Reconciler error for %s", customer_id)

    return stats


async def _count_active_projects(db: Any, customer_id: str) -> int:
    """Count projects with status == 'active' for a customer."""
    projects_ref = (
        db.collection("customers")
        .document(customer_id)
        .collection("projects")
        .where("status", "==", "active")
    )
    count = 0
    async for _ in projects_ref.stream():
        count += 1
    return count


async def _charge_overage(
    paddle_key: str,
    subscription_id: str,
    price_id: str,
    quantity: int,
) -> bool:
    """POST /subscriptions/{id}/charge to Paddle. Returns True on success."""
    url = f"{_PADDLE_API_BASE}/subscriptions/{subscription_id}/charge"
    headers = {
        "Authorization": f"Bearer {paddle_key}",
        "Content-Type": "application/json",
    }
    body = {
        "effective_from": "immediately",
        "items": [{"price_id": price_id, "quantity": quantity}],
    }
    try:
        async with httpx.AsyncClient(timeout=30.0) as client:
            resp = await client.post(url, json=body, headers=headers)
        if resp.status_code == 200:
            return True
        _log.error(
            "Paddle charge failed (%s) for sub %s: %s",
            resp.status_code,
            subscription_id,
            resp.text,
        )
        return False
    except Exception:
        _log.exception("Paddle charge exception for sub %s", subscription_id)
        return False


entry_point = usage_reconciler
