"""
BIBIVIDI — Billing
Savings-share billing: reads the executions table, computes tokens saved per
month, and applies tier pricing (20%/15%/10% of savings, with monthly caps).

Stripe integration fires when BIBIVIDI_STRIPE_API_KEY is set.

Pricing methodology (referenced in ToS):
  savings_eur = Σ (tokens_before - tokens_after) × model_price_per_token_eur
  bibividi_share_eur = min(savings_eur × tier_rate, monthly_cap)

Model prices stored at execution time for an immutable audit trail. If absent
(legacy rows), DEFAULT_PRICE_EUR is used as a conservative fallback.
"""
from __future__ import annotations

import calendar
import logging
from dataclasses import dataclass
from datetime import UTC, datetime

import aiosqlite

from bvd.config import settings

logger = logging.getLogger(__name__)


# ── Model price table ──────────────────────────────────────────────────────────
# Input token price in EUR per token. Update quarterly.
# Source: Anthropic/OpenAI pricing pages; USD→EUR at 0.92 (update as needed).
# These prices are used at request time and stored per-execution for audit trail.

MODEL_PRICES_EUR: dict[str, float] = {
    # Anthropic — Claude 4.x
    "claude-haiku-4-5-20251001":   0.80 / 1_000_000 * 0.92,
    "claude-haiku-4-5":            0.80 / 1_000_000 * 0.92,
    "claude-sonnet-4-6":           3.00 / 1_000_000 * 0.92,
    "claude-sonnet-4-5":           3.00 / 1_000_000 * 0.92,
    "claude-opus-4-6":            15.00 / 1_000_000 * 0.92,
    # Anthropic — Claude 3.x
    "claude-3-5-haiku-20241022":   0.80 / 1_000_000 * 0.92,
    "claude-3-5-sonnet-20241022":  3.00 / 1_000_000 * 0.92,
    "claude-3-haiku-20240307":     0.25 / 1_000_000 * 0.92,
    "claude-3-sonnet-20240229":    3.00 / 1_000_000 * 0.92,
    "claude-3-opus-20240229":     15.00 / 1_000_000 * 0.92,
    # OpenAI — GPT-4o family
    "gpt-4o":                      2.50 / 1_000_000 * 0.92,
    "gpt-4o-mini":                 0.15 / 1_000_000 * 0.92,
    # OpenAI — o-series
    "o1":                         15.00 / 1_000_000 * 0.92,
    "o1-mini":                     3.00 / 1_000_000 * 0.92,
    "o3-mini":                     1.10 / 1_000_000 * 0.92,
    # OpenAI — legacy
    "gpt-4-turbo":                10.00 / 1_000_000 * 0.92,
    "gpt-4":                      30.00 / 1_000_000 * 0.92,
    "gpt-3.5-turbo":               0.50 / 1_000_000 * 0.92,
}

# Fallback for unknown models (conservative: ~$1/M tokens)
DEFAULT_PRICE_EUR: float = 1.00 / 1_000_000 * 0.92

# ── Free-tier models — price = 0.0 ────────────────────────────────────────────
# Models on a free API tier; billing shows €0.00 and quota mode activates.
# Update quarterly when provider pricing pages change.
FREE_TIER_MODELS: frozenset[str] = frozenset({
    # Gemini free tier (via generativelanguage.googleapis.com)
    "gemini-1.5-flash",
    "gemini-1.5-flash-8b",
    "gemini-2.0-flash",
    "gemini-2.0-flash-lite",
    "gemini-2.0-flash-exp",
    # Groq free tier (via api.groq.com)
    "llama-3.3-70b-versatile",
    "llama-3.1-8b-instant",
    "llama3-8b-8192",
    "llama3-70b-8192",
    "mixtral-8x7b-32768",
    "gemma2-9b-it",
})

# Known daily input-token quotas for free tiers — update quarterly
# Sources: Gemini API docs; Groq API limits page
KNOWN_FREE_DAILY_TOKENS: dict[str, int] = {
    "gemini-1.5-flash":       1_000_000,
    "gemini-1.5-flash-8b":    1_000_000,
    "gemini-2.0-flash":       1_000_000,
    "gemini-2.0-flash-lite":  1_000_000,
    "gemini-2.0-flash-exp":   1_000_000,
    "llama-3.3-70b-versatile":  500_000,
    "llama-3.1-8b-instant":     500_000,
    "llama3-8b-8192":           500_000,
    "llama3-70b-8192":          500_000,
    "mixtral-8x7b-32768":       500_000,
    "gemma2-9b-it":             500_000,
}


def is_free_tier_model(model_name: str) -> bool:
    """Return True if model is on a free API tier (price=0, quota mode)."""
    key = model_name.lower().strip()
    return key in FREE_TIER_MODELS or any(key.startswith(m) for m in FREE_TIER_MODELS)


def free_tier_daily_tokens(model_name: str) -> int | None:
    """Return known daily token quota for a free-tier model, or None."""
    key = model_name.lower().strip()
    if key in KNOWN_FREE_DAILY_TOKENS:
        return KNOWN_FREE_DAILY_TOKENS[key]
    for name, limit in KNOWN_FREE_DAILY_TOKENS.items():
        if key.startswith(name):
            return limit
    return None


def model_price_eur(model_name: str) -> float:
    """Return price per token in EUR for a given model name.

    Free-tier models return 0.0. Unknown models fall back to DEFAULT_PRICE_EUR.
    """
    if is_free_tier_model(model_name):
        return 0.0
    key = model_name.lower().strip()
    if key in MODEL_PRICES_EUR:
        return MODEL_PRICES_EUR[key]
    # Prefix match: "claude-sonnet-4-6-20260101" → "claude-sonnet-4-6"
    for name, price in MODEL_PRICES_EUR.items():
        if key.startswith(name):
            return price
    return DEFAULT_PRICE_EUR


# ── Billing tiers ──────────────────────────────────────────────────────────────
# (rate, monthly_cap_eur)  — cap=inf means no cap
TIERS: dict[str, tuple[float, float]] = {
    "free":       (0.00,  0.0),
    "pro":        (0.20, 200.0),
    "growth":     (0.15, 500.0),
    "enterprise": (0.10, float("inf")),
}


@dataclass
class BillingResult:
    year: int
    month: int
    executions: int
    tokens_saved: int
    savings_eur: float         # raw € value of tokens saved
    bibividi_share_eur: float  # what BIBIVIDI charges (after tier rate + cap)
    tier: str
    tier_rate: float
    capped: bool               # True if monthly cap was applied


def compute_bill(
    savings_eur: float,
    tier: str,
    executions: int,
    tokens_saved: int,
    year: int,
    month: int,
) -> BillingResult:
    rate, cap = TIERS.get(tier, TIERS["pro"])
    raw_share = savings_eur * rate
    share = min(raw_share, cap) if cap != float("inf") else raw_share
    return BillingResult(
        year=year,
        month=month,
        executions=executions,
        tokens_saved=tokens_saved,
        savings_eur=round(savings_eur, 4),
        bibividi_share_eur=round(share, 4),
        tier=tier,
        tier_rate=rate,
        capped=(cap != float("inf") and raw_share > cap),
    )


async def monthly_billing(year: int, month: int, tier: str | None = None) -> BillingResult:
    """Compute the billing for a given month by reading the executions table.

    Uses the per-row ``model_price_per_token_eur`` for accuracy.
    Falls back to ``DEFAULT_PRICE_EUR`` for legacy rows without a price.
    """
    effective_tier = tier or settings.billing_tier
    _, last_day = calendar.monthrange(year, month)
    start = datetime(year, month, 1, tzinfo=UTC).isoformat()
    end = datetime(year, month, last_day, 23, 59, 59, tzinfo=UTC).isoformat()

    async with aiosqlite.connect(settings.db_path) as db:  # type: ignore[arg-type]
        async with db.execute(
            """
            SELECT
                COUNT(*)                                          AS executions,
                COALESCE(SUM(tokens_before - tokens_after), 0)   AS tokens_saved,
                COALESCE(SUM(
                    (tokens_before - tokens_after) *
                    COALESCE(model_price_per_token_eur, ?)
                ), 0)                                            AS savings_eur
            FROM executions
            WHERE executed_at >= ? AND executed_at <= ?
              AND tokens_before > tokens_after
            """,
            (DEFAULT_PRICE_EUR, start, end),
        ) as cur:
            row = await cur.fetchone()

    executions = int(row[0]) if row else 0
    tokens_saved = int(row[1]) if row else 0
    savings_eur = float(row[2]) if row else 0.0

    result = compute_bill(savings_eur, effective_tier, executions, tokens_saved, year, month)

    # Report savings to billing API (fire-and-forget; failure is non-fatal)
    from bvd.license import hash_license_key, load_license_key
    key = load_license_key()
    if key:
        headers: dict[str, str] = {"X-BIBIVIDI-License": key}
        try:
            await _billing_http_post(
                f"{settings.billing_api_url}/v1/billing/report",
                headers=headers,
                json={
                    "year": year, "month": month, "tier": effective_tier,
                    "tokens_saved": result.tokens_saved,
                    "savings_eur": result.savings_eur,
                    "bibividi_share_eur": result.bibividi_share_eur,
                    "license_key_hash": hash_license_key(key) if key else None,
                },
            )
        except Exception as exc:
            logger.debug("Billing API report skipped: %s", exc)

    return result


async def _billing_http_post(url: str, *, headers: dict, json: dict, **kw) -> object:
    """Internal HTTP helper for billing API calls. Extracted for testability."""
    import httpx
    async with httpx.AsyncClient(timeout=10.0) as client:
        return await client.post(url, headers=headers, json=json, **kw)


async def run_stripe_billing(result: BillingResult) -> bool:
    """Create a Stripe invoice item for the billing period.

    Returns True on success, False if Stripe is not configured or the call fails.
    Only bills if ``bibividi_share_eur > 0`` and a Stripe key is configured.
    """
    if not settings.stripe_api_key:
        logger.debug("Stripe billing skipped: BIBIVIDI_STRIPE_API_KEY not set")
        return False

    if result.bibividi_share_eur <= 0:
        logger.info("Billing skipped: nothing to charge (€0.00)")
        return False

    try:
        import stripe  # type: ignore[import-untyped]
        stripe.api_key = settings.stripe_api_key

        cap_note = " (cap applied)" if result.capped else ""
        description = (
            f"BIBIVIDI savings-share {result.year}-{result.month:02d}: "
            f"{result.tokens_saved:,} tokens saved · "
            f"tier={result.tier} ({result.tier_rate * 100:.0f}%){cap_note} · "
            f"€{result.savings_eur:.4f} gross savings"
        )
        stripe.InvoiceItem.create(
            amount=int(result.bibividi_share_eur * 100),  # Stripe uses cents
            currency="eur",
            description=description,
        )
        logger.info(
            "Stripe invoice item created: €%.2f for %d-%02d",
            result.bibividi_share_eur, result.year, result.month,
        )
        return True

    except ImportError:
        logger.warning("Stripe package not installed — pip install stripe")
        return False
    except Exception as exc:
        logger.error("Stripe billing failed: %s", exc)
        return False


# ── Stripe webhook stubs (real implementation lives in bibividi/website) ───────
# These stubs exist so that test_license.py can mock and verify the contracts.
# The production webhook endpoint runs as a publicly-deployed HTTPS service —
# the local proxy cannot receive inbound Stripe webhooks.


class WebhookSignatureError(Exception):
    """Raised when the Stripe-Signature header is missing or invalid."""


def verify_stripe_signature(payload: str | bytes, stripe_signature: str) -> None:
    """Verify the Stripe webhook signature. Raises WebhookSignatureError on failure.

    Real implementation: bibividi/website — uses stripe.Webhook.construct_event().
    This stub is a no-op: tests that check invalid-signature behaviour must patch
    it with side_effect=WebhookSignatureError.
    """
    pass  # no-op stub — real validation in bibividi/website


async def key_exists_for_session(session_id: str) -> bool:
    """Return True if a license key was already issued for this Stripe session_id.

    Real implementation: bibividi/website — queries the key store.
    This stub always returns False (no key stored yet).
    """
    return False  # stub — real check in bibividi/website


async def save_license_key(key: str, customer_id: str, session_id: str) -> None:
    """Persist a newly issued license key server-side (key → Stripe customer mapping).

    Real implementation: bibividi/website.
    This stub is here for test mocking only.
    """
    raise NotImplementedError("Key store lives in bibividi/website")


async def send_activation_email(*, to: str, body: str) -> None:
    """Send the license key activation email via Resend/Postmark.

    Real implementation: bibividi/website.
    This stub is here for test mocking only.
    """
    raise NotImplementedError("Mailer lives in bibividi/website")


async def handle_stripe_webhook(event: dict, stripe_signature: str) -> None:
    """Handle an incoming Stripe webhook event.

    Real implementation: bibividi/website — POST /webhooks/stripe.
    This stub exists only for test_license.py to mock the contracts.

    Raises WebhookSignatureError on bad signature.
    Silently ignores unrelated event types.
    On checkout.session.completed: generates a bvd-xxxxxxxx key, persists it,
    and sends the activation email — unless a key already exists for the session.
    """
    import uuid

    verify_stripe_signature("", stripe_signature)  # raises WebhookSignatureError if bad

    if event.get("type") != "checkout.session.completed":
        return

    obj = event["data"]["object"]
    session_id: str = obj["id"]
    customer_id: str = obj["customer"]
    email: str = obj["customer_email"]

    if await key_exists_for_session(session_id):
        logger.info("Duplicate webhook for session %s — skipping", session_id)
        return

    key = f"bvd-{uuid.uuid4().hex[:12]}"
    await save_license_key(key, customer_id, session_id)
    await send_activation_email(
        to=email,
        body=(
            f"Your BIBIVIDI license key: {key}\n\n"
            f"Activate with: bibividi activate {key}"
        ),
    )
