"""Stripe integration client.

Implements the StripeClient protocol from billing.interfaces.


Supports:
  - Customer management (linked to P2HS users/workspaces)
  - Checkout Sessions for predefined subscription plans
  - Customer Portal for self-service billing management
  - Subscription lifecycle management
  - Usage-based metered billing (report usage records)
  - Webhook signature verification

Stripe webhook events to subscribe to (configure in Stripe Dashboard → Webhooks):

  Subscription & Checkout:
  - checkout.session.completed
  - customer.subscription.created
  - customer.subscription.updated
  - customer.subscription.deleted
  - invoice.paid
  - invoice.payment_failed

  Account & Customer:
  - account.updated
  - customer.created
  - customer.updated

  Charges & Refunds:
  - charge.captured
  - charge.failed
  - charge.refunded
  - charge.refund.updated
  - charge.succeeded

  Disputes:
  - charge.dispute.created
  - charge.dispute.closed

  PaymentIntents (one-time payments, SCA):
  - payment_intent.amount_capturable_updated
  - payment_intent.payment_failed
  - payment_intent.processing
  - payment_intent.requires_action
  - payment_intent.succeeded

  SetupIntents (payment method setup):
  - setup_intent.succeeded
  - setup_intent.setup_failed

  Radar / Fraud:
  - review.opened
  - review.closed

  Sources (legacy):
  - source.canceled
  - source.chargeable
"""
from __future__ import annotations

import json
from asyncio import to_thread
from typing import Any, Optional

import stripe
import structlog

from src.config import get_settings

logger = structlog.get_logger()


def _stripe_obj_get(obj: Any, key: str, default: Any = None) -> Any:
    """Safe field read for Stripe SDK objects (missing keys must not raise)."""
    if obj is None:
        return default
    getter = getattr(obj, "get", None)
    if callable(getter):
        return getter(key, default)
    return getattr(obj, key, default)


def _subscription_period_bounds(
    sub: Any,
    recurring_items: list[Any],
) -> tuple[int | None, int | None]:
    """Billing period on subscription root or, for Clover-era APIs, on line items."""
    period_start = _stripe_obj_get(sub, "current_period_start")
    period_end = _stripe_obj_get(sub, "current_period_end")
    if period_start is not None and period_end is not None:
        return period_start, period_end
    for item in recurring_items:
        ps = _stripe_obj_get(item, "current_period_start")
        pe = _stripe_obj_get(item, "current_period_end")
        if period_start is None:
            period_start = ps
        if period_end is None:
            period_end = pe
        if period_start is not None and period_end is not None:
            break
    return period_start, period_end


# Events to subscribe to in Stripe Dashboard (Developers → Webhooks → Add endpoint)
STRIPE_WEBHOOK_EVENTS = [
    # Account
    "account.updated",
    # Charges
    "charge.captured",
    "charge.dispute.closed",
    "charge.dispute.created",
    "charge.failed",
    "charge.refund.updated",
    "charge.refunded",
    "charge.succeeded",
    # PaymentIntents
    "payment_intent.amount_capturable_updated",
    "payment_intent.payment_failed",
    "payment_intent.processing",
    "payment_intent.requires_action",
    "payment_intent.succeeded",
    # Reviews (Radar/fraud)
    "review.closed",
    "review.opened",
    # SetupIntents
    "setup_intent.setup_failed",
    "setup_intent.succeeded",
    # Sources (legacy)
    "source.canceled",
    "source.chargeable",
    # Subscriptions & Checkout (core billing)
    "checkout.session.completed",
    "customer.subscription.created",
    "customer.subscription.updated",
    "customer.subscription.deleted",
    "invoice.paid",
    "invoice.payment_failed",
]


class StripeClientImpl:
    def __init__(self, api_key: Optional[str] = None, webhook_secret: Optional[str] = None):
        settings = get_settings()
        self._api_key = api_key or settings.stripe_secret_key
        self._webhook_secret = webhook_secret or settings.stripe_webhook_secret
        stripe.api_key = self._api_key

    async def create_customer(self, email: str, workspace_name: str) -> str:
        customer = stripe.Customer.create(
            email=email,
            name=workspace_name,
            metadata={"source": "p2hs", "platform": "p2hs"},
        )
        logger.info("stripe_customer_created", customer_id=customer.id)
        return customer.id

    async def create_subscription_checkout(
        self,
        customer_id: str,
        price_id: str,
        success_url: str,
        cancel_url: str,
        metadata: Optional[dict] = None,
        idempotency_key: Optional[str] = None,
    ) -> tuple[str, str]:
        meta = dict(metadata or {})
        kwargs = dict(
            customer=customer_id,
            payment_method_types=["card"],
            line_items=[{"price": price_id, "quantity": 1}],
            mode="subscription",
            success_url=success_url,
            cancel_url=cancel_url,
            allow_promotion_codes=True,
            metadata=meta,
            subscription_data={"metadata": meta},
        )
        if idempotency_key:
            kwargs["idempotency_key"] = idempotency_key
        session = stripe.checkout.Session.create(**kwargs)
        logger.info(
            "stripe_checkout_subscription_created",
            session_id=session.id,
            workspace_id=meta.get("workspace_id"),
            user_id=meta.get("user_id"),
        )
        return session.url, session.id

    async def create_payment_checkout(
        self,
        customer_id: str,
        price_id: str,
        success_url: str,
        cancel_url: str,
        metadata: Optional[dict] = None,
        idempotency_key: Optional[str] = None,
    ) -> tuple[str, str]:
        """One-time payment checkout (e.g. usage credit top-up)."""
        meta = dict(metadata or {})
        kwargs = dict(
            customer=customer_id,
            payment_method_types=["card"],
            line_items=[{"price": price_id, "quantity": 1}],
            mode="payment",
            success_url=success_url,
            cancel_url=cancel_url,
            metadata=meta,
        )
        if idempotency_key:
            kwargs["idempotency_key"] = idempotency_key
        session = stripe.checkout.Session.create(**kwargs)
        logger.info("stripe_checkout_payment_created", session_id=session.id)
        return session.url, session.id

    async def create_customer_portal(
        self, customer_id: str, return_url: Optional[str] = None
    ) -> str:
        from src.config import get_settings
        url = return_url or get_settings().app_url
        session = stripe.billing_portal.Session.create(
            customer=customer_id,
            return_url=url,
        )
        return session.url

    async def create_subscription_update_portal(
        self, customer_id: str, subscription_id: str, price_id: str, return_url: str
    ) -> str:
        from src.core.exceptions import ValidationError

        sub = await to_thread(stripe.Subscription.retrieve, subscription_id)
        if sub.get("customer") != customer_id:
            raise ValidationError("Subscription does not belong to this billing customer")
        items = sub.get("items", {}).get("data", [])
        if len(items) != 1:
            raise ValidationError("Manage this subscription through the billing portal")
        session = await to_thread(
            stripe.billing_portal.Session.create,
            customer=customer_id,
            return_url=return_url,
            flow_data={
                "type": "subscription_update_confirm",
                "subscription_update_confirm": {
                    "subscription": subscription_id,
                    "items": [{
                        "id": items[0]["id"],
                        "price": price_id,
                        "quantity": items[0].get("quantity", 1),
                    }],
                },
                "after_completion": {"type": "redirect", "redirect": {"return_url": return_url}},
            },
        )
        return session.url

    async def get_subscription(self, subscription_id: str) -> dict:
        sub = stripe.Subscription.retrieve(subscription_id, expand=["items.data.price"])
        items = sub.get("items", {}).get("data", [])
        recurring_items = [i for i in items if i.get("price", {}).get("recurring")]
        plan_price = recurring_items[0]["price"] if recurring_items else None
        plan_id = plan_price["id"] if plan_price else None
        plan_name = plan_price.get("nickname") if plan_price else None
        stripe_product_id = plan_price.get("product") if plan_price else None
        interval = (
            plan_price.get("recurring", {}).get("interval") if plan_price else None
        )

        metered_item = next(
            (
                item["id"]
                for item in items
                if item.get("price", {}).get("recurring", {}).get("meter")
                # legacy: usage_type=metered (pre-2025-03-31.basil API)
                or item.get("price", {}).get("recurring", {}).get("usage_type") == "metered"
            ),
            None,
        )

        period_start, period_end = _subscription_period_bounds(sub, recurring_items)

        return {
            "id": sub.id,
            "customer": sub.customer,
            "status": sub.status,
            "plan_id": plan_id,
            "plan_name": plan_name,
            "stripe_product_id": stripe_product_id,
            "billing_interval": interval,
            "current_period_start": period_start,
            "current_period_end": period_end,
            "cancel_at_period_end": _stripe_obj_get(sub, "cancel_at_period_end", False),
            "canceled_at": _stripe_obj_get(sub, "canceled_at"),
            "trial_start": _stripe_obj_get(sub, "trial_start"),
            "trial_end": _stripe_obj_get(sub, "trial_end"),
            "metered_item_id": metered_item,
        }

    def list_subscriptions(self, customer_id: str) -> list[dict]:
        """List subscriptions for customer (for reconciliation)."""
        subs = stripe.Subscription.list(
            customer=customer_id,
            limit=20,
            expand=["data.items.data.price"],
        )
        out = []
        for s in subs.data:
            items = getattr(s, "items", None) or {}
            items_data = getattr(items, "data", []) or []
            recurring = [
                i for i in items_data
                if getattr(getattr(i, "price", None), "recurring", None)
            ]
            plan_price = recurring[0].price if recurring else None
            plan_id = plan_price.id if plan_price else None
            cps, cpe = _subscription_period_bounds(s, recurring)
            out.append({
                "id": s.id,
                "customer": s.customer,
                "status": s.status,
                "current_period_start": cps,
                "current_period_end": cpe,
                "cancel_at_period_end": _stripe_obj_get(s, "cancel_at_period_end", False),
                "canceled_at": _stripe_obj_get(s, "canceled_at"),
                "plan_id": plan_id,
            })
        return out

    def get_checkout_session(self, session_id: str) -> dict:
        """Retrieve checkout session (for webhook fulfillment)."""
        session = stripe.checkout.Session.retrieve(session_id)
        return {
            "id": session.id,
            "customer": session.customer,
            "subscription": session.subscription,
            "payment_intent": session.payment_intent,
            "payment_status": session.payment_status,
            "metadata": dict(session.metadata or {}),
        }

    async def list_invoices(self, customer_id: str) -> list[dict]:
        invoices = stripe.Invoice.list(customer=customer_id, limit=50)
        return [
            {
                "id": inv.id,
                "customer": inv.customer,
                "status": inv.status,
                "amount_due": inv.amount_due,
                "amount_paid": inv.amount_paid,
                "currency": inv.currency,
                "hosted_invoice_url": inv.hosted_invoice_url,
                "invoice_pdf": inv.invoice_pdf,
            }
            for inv in invoices.data
        ]

    async def create_invoice_item(
        self,
        stripe_customer_id: str,
        *,
        amount_minor: int,
        currency: str,
        description: str,
        idempotency_key: Optional[str] = None,
    ) -> str:
        """Add a one-off line item to the customer's next invoice.

        Used for managed-email-domain overage (src/workers/tasks/bill_email_overage.py) —
        deliberately NOT a Stripe Meter, since the $/1000 rate is a Core DB/env setting that
        must be repriceable without touching the Stripe catalog. Returns the InvoiceItem id.
        """
        kwargs: dict = {
            "customer": stripe_customer_id,
            "amount": amount_minor,
            "currency": currency,
            "description": description,
        }
        if idempotency_key:
            kwargs["idempotency_key"] = idempotency_key
        item = stripe.InvoiceItem.create(**kwargs)
        logger.info(
            "stripe_invoice_item_created",
            customer=stripe_customer_id,
            amount_minor=amount_minor,
            invoice_item_id=item.id,
        )
        return item.id

    async def report_usage(self, event_name: str, stripe_customer_id: str, quantity: int, timestamp: Optional[int] = None) -> None:
        """Report usage via Stripe Billing Meter Events (API 2025-03-31.basil+)."""
        payload: dict = {
            "value": str(quantity),
            "stripe_customer_id": stripe_customer_id,
        }
        kwargs: dict = {"event_name": event_name, "payload": payload}
        if timestamp is not None:
            kwargs["timestamp"] = timestamp
        stripe.billing.MeterEvent.create(**kwargs)
        logger.info("stripe_meter_event_reported", event_name=event_name, customer=stripe_customer_id, quantity=quantity)

    async def create_product(self, name: str, description: Optional[str] = None, metadata: Optional[dict] = None) -> dict:
        product = stripe.Product.create(
            name=name,
            description=description or "",
            metadata=metadata or {},
        )
        logger.info("stripe_product_created", product_id=product.id)
        return {"id": product.id, "name": product.name}

    async def create_price(
        self, product_id: str, unit_amount: int, currency: str = "usd",
        interval: str = "month", nickname: Optional[str] = None,
        interval_count: int = 1,
        recurring: bool = True,
    ) -> dict:
        kwargs = dict(
            product=product_id,
            unit_amount=unit_amount,
            currency=currency,
            nickname=nickname,
        )
        if recurring:
            kwargs["recurring"] = {"interval": interval, "interval_count": interval_count}
        price = stripe.Price.create(**kwargs)
        logger.info("stripe_price_created", price_id=price.id, amount=unit_amount)
        return {"id": price.id, "unit_amount": price.unit_amount}

    async def create_meter(self, event_name: str, display_name: str) -> dict:
        """Create a Stripe Billing Meter (required before creating a metered price, API 2025-03-31.basil+)."""
        meter = stripe.billing.Meter.create(
            display_name=display_name,
            event_name=event_name,
            default_aggregation={"formula": "sum"},
            customer_mapping={
                "event_payload_key": "stripe_customer_id",
                "type": "by_id",
            },
            value_settings={"event_payload_key": "value"},
        )
        logger.info("stripe_meter_created", meter_id=meter.id, event_name=event_name)
        return {"id": meter.id, "event_name": meter.event_name}

    async def create_metered_price(
        self, product_id: str, meter_id: str, currency: str = "usd",
        unit_amount: int = 0, nickname: Optional[str] = None,
    ) -> dict:
        """Create a meter-backed recurring price (API 2025-03-31.basil+)."""
        price = stripe.Price.create(
            product=product_id,
            currency=currency,
            billing_scheme="per_unit",
            unit_amount=unit_amount,
            recurring={"interval": "month", "meter": meter_id, "usage_type": "metered"},
            nickname=nickname,
        )
        logger.info("stripe_metered_price_created", price_id=price.id, meter_id=meter_id)
        return {"id": price.id}

    async def update_product(self, product_id: str, **kwargs) -> dict:
        product = stripe.Product.modify(product_id, **kwargs)
        logger.info("stripe_product_updated", product_id=product.id)
        return {"id": product.id}

    async def retrieve_price(self, price_id: str) -> Optional[dict]:
        """Fetch a Price from Stripe for amount/currency comparison (catalog sync)."""
        try:
            price = stripe.Price.retrieve(price_id)
            rec = getattr(price, "recurring", None)
            recurring = None
            if rec:
                recurring = {
                    "interval": getattr(rec, "interval", None),
                    "usage_type": getattr(rec, "usage_type", None),
                }
            return {
                "id": price.id,
                "unit_amount": price.unit_amount,
                "currency": (price.currency or "").lower(),
                "active": price.active,
                "recurring": recurring,
            }
        except Exception as e:
            logger.warning("stripe_price_retrieve_failed", price_id=price_id, error=str(e))
            return None

    async def deactivate_price(self, price_id: str) -> None:
        stripe.Price.modify(price_id, active=False)
        logger.info("stripe_price_deactivated", price_id=price_id)

    async def create_one_time_price(
        self,
        product_id: str,
        unit_amount: int,
        currency: str = "usd",
        nickname: Optional[str] = None,
    ) -> dict:
        price = stripe.Price.create(
            product=product_id,
            unit_amount=unit_amount,
            currency=currency,
            nickname=nickname,
        )
        logger.info("stripe_one_time_price_created", price_id=price.id, amount=unit_amount)
        return {"id": price.id, "unit_amount": price.unit_amount}

    def verify_webhook_signature(self, payload: bytes, sig_header: str) -> dict:
        stripe.Webhook.construct_event(payload, sig_header, self._webhook_secret)
        # Decode the verified bytes to plain JSON types. SDK Data objects are
        # not JSON-serializable in all Stripe versions and must not reach JSONB.
        event = json.loads(payload)
        return {
            "id": event["id"],
            "type": event["type"],
            "data": event["data"],
        }
