"""Worker task — bill accrued managed-email-domain overage as Stripe invoice items.

Overage units accrue on ``WorkspaceEmailQuota.pending_overage_units`` every time
``EmailQuotaService.check_and_reserve`` allows a send past the workspace's included
monthly amount (src/domain/email_domains/service.py). This task periodically converts
that counter into a real charge — a one-off Stripe Invoice Item, never a Meter, since the
$/1000 rate is a Core DB/env setting that must be repriceable without touching the Stripe
catalog (src/config.py::email_overage_rate_per_1000_usd_cents, or a per-workspace override
on WorkspaceEmailQuota.override_rate_per_1000_minor).

Every charge is recorded in WorkspaceEmailOverageCharge before pending_overage_units is
cleared, and the Stripe call carries an idempotency key derived from that ledger row, so a
retried task run can never double-bill the same units.
"""
from __future__ import annotations

import math
from typing import Optional

import structlog

logger = structlog.get_logger()


async def bill_email_overage(ctx: dict) -> dict:
    from src.config import get_settings
    from src.core.database import async_session_factory
    from src.domain.billing.access_policy import workspace_has_active_billing
    from src.domain.billing.repository import SubscriptionRepository
    from src.domain.email_domains.repository import EmailOverageChargeRepository, EmailQuotaRepository
    from src.integrations.stripe_client import StripeClientImpl

    settings = get_settings()
    db_factory = ctx.get("db_factory") or async_session_factory

    billed = 0
    skipped = 0

    async with db_factory() as db:
        quota_repo = EmailQuotaRepository(db)
        charge_repo = EmailOverageChargeRepository(db)
        sub_repo = SubscriptionRepository(db)
        stripe: Optional[StripeClientImpl] = StripeClientImpl() if settings.stripe_secret_key else None

        for q in await quota_repo.list_with_pending_overage():
            units = q.pending_overage_units
            if units <= 0:
                continue

            sub = await sub_repo.get_by_workspace(q.workspace_id)
            if not sub or not sub.stripe_customer_id or not workspace_has_active_billing(sub.status):
                # Lost billability since the units accrued (e.g. subscription cancelled).
                # Leave pending_overage_units untouched — bill it if/when they resubscribe,
                # never silently write it off.
                skipped += 1
                logger.warning(
                    "email_overage_billing_skipped_no_subscription",
                    workspace_id=str(q.workspace_id),
                    pending_units=units,
                )
                continue

            rate_per_1000_minor = q.override_rate_per_1000_minor or settings.email_overage_rate_per_1000_usd_cents
            amount_minor = math.ceil(units * rate_per_1000_minor / 1000)
            if amount_minor <= 0:
                q.pending_overage_units = 0
                await db.flush()
                continue

            charge = await charge_repo.create(
                q.workspace_id,
                period_start=q.period_start,
                period_end=q.period_end,
                overage_units=units,
                rate_per_1000_minor=rate_per_1000_minor,
                amount_minor=amount_minor,
                stripe_invoice_item_id=None,
            )

            stripe_invoice_item_id = None
            if stripe is not None:
                try:
                    stripe_invoice_item_id = await stripe.create_invoice_item(
                        sub.stripe_customer_id,
                        amount_minor=amount_minor,
                        currency="usd",
                        description=f"Managed email domain overage — {units} emails over the included plan quota",
                        idempotency_key=f"email-overage-{charge.id}",
                    )
                except Exception as exc:
                    logger.error(
                        "email_overage_stripe_invoice_item_failed",
                        workspace_id=str(q.workspace_id),
                        charge_id=str(charge.id),
                        error=str(exc),
                    )
                    # Ledger row stays for audit; pending_overage_units NOT cleared, so the
                    # next run retries this same batch (idempotency_key keeps it from
                    # double-charging Stripe if the create actually succeeded upstream).
                    await db.commit()
                    continue

            charge.stripe_invoice_item_id = stripe_invoice_item_id
            q.pending_overage_units = 0
            await db.flush()
            await db.commit()
            billed += 1
            logger.info(
                "email_overage_billed",
                workspace_id=str(q.workspace_id),
                units=units,
                amount_minor=amount_minor,
                stripe_invoice_item_id=stripe_invoice_item_id,
            )

    return {"billed": billed, "skipped": skipped}
