"""
Stripe payment integration for subscriptions and credits
"""
import json
import os
import logging
import re
import threading
from pathlib import Path
from typing import Optional, Dict, List, Any, Tuple
from datetime import datetime, timezone

logger = logging.getLogger(__name__)

# Try to import stripe
try:
    import stripe
    STRIPE_AVAILABLE = True
except ImportError:
    STRIPE_AVAILABLE = False
    stripe = None

from models.subscription import PlanType, PLANS, CREDIT_PACKAGES, SubscriptionStatus
from services.auth_service import get_user_by_id, update_user, add_credits
from services.pricing_config import (
    get_subscription_line_amount_eur,
    stripe_price_ids_for_plan,
    stripe_mode,
)


# Stripe configuration
STRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET", "")
STRIPE_PUBLISHABLE_KEY = os.getenv("STRIPE_PUBLISHABLE_KEY", "")


def _get_stripe_secret_key() -> str:
    """Read Stripe secret key at runtime to support hot-reload/admin updates."""
    return os.getenv("STRIPE_SECRET_KEY", "").strip()


def _ensure_stripe_client_configured() -> bool:
    """Configure Stripe SDK lazily with the latest key from environment."""
    if not STRIPE_AVAILABLE:
        return False
    secret = _get_stripe_secret_key()
    if not secret:
        return False
    stripe.api_key = secret
    return True


def is_stripe_configured() -> bool:
    """Check if Stripe is properly configured"""
    return _ensure_stripe_client_configured()


# ═══════════════════════════════════════════════════════════════════════════
# Codes promo (réductions) — stockés dans data/promo_codes.json (comme
# pricing_overrides.json : JSON géré par l'admin, pas de table en base).
#
# Chaque entrée :
#   code, active, type ("percent"|"amount"), value, plans (["all"] ou ids),
#   max_redemptions (None = illimité), times_used, expires_at (ISO ou None),
#   stripe_coupon_id / stripe_promotion_code_id (None si créé hors Stripe),
#   stripe_mode ("test"|"live"|"local_only"), created_at, deactivated_at.
#
# Double plafond : le compteur local (times_used) est incrémenté uniquement à
# la complétion du paiement, ET Stripe applique son propre max_redemptions.
# ═══════════════════════════════════════════════════════════════════════════

try:
    from config import config as _app_config

    PROMO_CODES_FILE: Path = _app_config.BASE_DIR / "data" / "promo_codes.json"
except Exception:  # pragma: no cover — config toujours disponible en pratique
    PROMO_CODES_FILE = Path("data") / "promo_codes.json"

PROMO_CODE_RE = re.compile(r"^[A-Z0-9-]{3,24}$")
_promo_lock = threading.Lock()


class PromoValidationError(Exception):
    """Code promo inutilisable au checkout — la cause est explicite."""

    def __init__(self, reason: str, message: str):
        self.reason = reason  # unknown|inactive|expired|exhausted|plan|local_only
        self.message = message
        super().__init__(message)


class DuplicatePromoError(Exception):
    """Un code identique existe déjà (vérification faite SOUS le verrou)."""


class StripePromoCreationError(Exception):
    """Stripe a refusé la création du Coupon/Promotion Code (rien n'est persisté)."""


def load_promo_codes() -> List[dict]:
    try:
        if PROMO_CODES_FILE.exists():
            with open(PROMO_CODES_FILE, encoding="utf-8") as f:
                data = json.load(f)
            return data if isinstance(data, list) else []
    except Exception as e:
        logger.warning("Lecture promo_codes impossible: %s", e)
    return []


def save_promo_codes(entries: List[dict]) -> None:
    """Écriture atomique : fichier temporaire puis renommage (os.replace) —
    un plantage pendant l'écriture ne peut pas laisser un JSON tronqué."""
    PROMO_CODES_FILE.parent.mkdir(parents=True, exist_ok=True)
    tmp_path = PROMO_CODES_FILE.with_name(PROMO_CODES_FILE.name + ".tmp")
    with open(tmp_path, "w", encoding="utf-8") as f:
        json.dump(entries, f, indent=2, ensure_ascii=False)
    os.replace(tmp_path, PROMO_CODES_FILE)


def _find_promo(entries: List[dict], code: str) -> Optional[dict]:
    wanted = (code or "").strip().upper()
    for e in entries:
        if (e.get("code") or "").strip().upper() == wanted:
            return e
    return None


def validate_promo_code(
    code: str, plan: str, entries: Optional[List[dict]] = None
) -> dict:
    """Valide un code promo AVANT tout appel Stripe — lève PromoValidationError.

    Causes explicites : inconnu, désactivé, expiré, épuisé, forfait non couvert,
    ou code créé « local_only » (sans Stripe, donc inapplicable au checkout).
    Une limite ou un compteur illisible (JSON corrompu) est traité par prudence
    comme un code épuisé, jamais comme illimité.
    """
    wanted = (code or "").strip().upper()
    if not wanted:
        raise PromoValidationError("unknown", "Aucun code promo fourni.")
    promo = _find_promo(entries if entries is not None else load_promo_codes(), wanted)
    if not promo:
        raise PromoValidationError("unknown", f"Le code promo « {wanted} » n'existe pas.")
    if not promo.get("active", False):
        raise PromoValidationError("inactive", f"Le code promo « {wanted} » n'est plus actif.")

    expires_at = promo.get("expires_at")
    if expires_at:
        try:
            exp = datetime.fromisoformat(str(expires_at))
            if exp.tzinfo is None:
                exp = exp.replace(tzinfo=timezone.utc)
            if datetime.now(timezone.utc) >= exp:
                raise PromoValidationError(
                    "expired", f"Le code promo « {wanted} » a expiré."
                )
        except PromoValidationError:
            raise
        except (TypeError, ValueError):
            logger.warning("promo %s: expires_at illisible (%s)", wanted, expires_at)

    max_redemptions = promo.get("max_redemptions")
    if max_redemptions is not None:
        try:
            limit = int(max_redemptions)
            used = int(promo.get("times_used", 0) or 0)
        except (TypeError, ValueError):
            raise PromoValidationError(
                "exhausted",
                f"Le code promo « {wanted} » est inutilisable : sa limite "
                "d'utilisations est illisible.",
            )
        if used >= limit:
            raise PromoValidationError(
                "exhausted",
                f"Le code promo « {wanted} » a atteint sa limite d'utilisations.",
            )

    plans = promo.get("plans") or ["all"]
    if "all" not in plans and plan not in plans:
        raise PromoValidationError(
            "plan",
            f"Le code promo « {wanted} » n'est pas valable pour le forfait « {plan} ».",
        )

    if not promo.get("stripe_promotion_code_id"):
        raise PromoValidationError(
            "local_only",
            f"Le code promo « {wanted} » n'est pas relié à Stripe et ne peut pas "
            "être appliqué au paiement.",
        )
    return promo


def increment_promo_usage(code: str, session_id: str = "") -> bool:
    """Incrémente le compteur local d'un code promo (à la complétion SEULEMENT).

    ``session_id`` (identifiant de session Stripe de checkout) protège du double
    comptage : un webhook suivi d'une synchronisation manuelle du MÊME paiement
    ne compte qu'une fois, y compris en mode JSON (sans base).
    """
    wanted = (code or "").strip().upper()
    if not wanted:
        return False
    sid = (session_id or "").strip()
    with _promo_lock:
        entries = load_promo_codes()
        promo = _find_promo(entries, wanted)
        if not promo:
            return False
        counted = promo.get("counted_sessions") or []
        if sid and sid in counted:
            logger.info(
                "promo %s: checkout session %s déjà comptée — incrément ignoré",
                wanted, sid,
            )
            return False
        try:
            promo["times_used"] = int(promo.get("times_used", 0) or 0) + 1
        except (TypeError, ValueError):
            promo["times_used"] = 1
        promo["last_used_at"] = datetime.now(timezone.utc).isoformat()
        if sid:
            promo["counted_sessions"] = list(counted) + [sid]
        save_promo_codes(entries)
    logger.info("promo_code usage incremented: %s → %s", wanted, promo["times_used"])
    return True


def deactivate_promo_code(code: str) -> Optional[dict]:
    """Désactive un code localement ET chez Stripe (promotion code désactivée)."""
    wanted = (code or "").strip().upper()
    with _promo_lock:
        entries = load_promo_codes()
        promo = _find_promo(entries, wanted)
        if not promo:
            return None
        promo["active"] = False
        promo["deactivated_at"] = datetime.now(timezone.utc).isoformat()
        if promo.get("stripe_promotion_code_id") and is_stripe_configured():
            try:
                stripe.PromotionCode.modify(promo["stripe_promotion_code_id"], active=False)
            except Exception as e:
                # Stripe peut être injoignable : la désactivation locale reste
                # effective et fait foi pour le checkout.
                logger.error("Stripe PromotionCode.deactivate failed for %s: %s", wanted, e)
        save_promo_codes(entries)
    return promo


def delete_promo_code(code: str) -> Optional[dict]:
    """Supprime un code promo (admin). La promotion code Stripe associée est
    supprimée aussi (repli : désactivée si la suppression est refusée)."""
    wanted = (code or "").strip().upper()
    with _promo_lock:
        entries = load_promo_codes()
        promo = _find_promo(entries, wanted)
        if not promo:
            return None
        if promo.get("stripe_promotion_code_id") and is_stripe_configured():
            try:
                stripe.PromotionCode.delete(promo["stripe_promotion_code_id"])
            except Exception as e:
                logger.warning(
                    "Stripe PromotionCode.delete failed for %s (%s) — fallback: deactivate",
                    wanted, e,
                )
                try:
                    stripe.PromotionCode.modify(promo["stripe_promotion_code_id"], active=False)
                except Exception as e2:
                    logger.error("Stripe PromotionCode deactivate fallback failed: %s", e2)
        entries = [e for e in entries if e is not promo]
        save_promo_codes(entries)
    logger.info("promo %s supprimé", wanted)
    return promo


def _create_stripe_refs(
    code: str, type: str, value: float,
    max_redemptions: Optional[int], expires_at: Optional[str],
) -> tuple:
    """Crée le Coupon + Promotion Code Stripe. En cas d'échec Stripe, supprime
    un coupon déjà créé (pas d'objet orphelin) et lève StripePromoCreationError."""
    coupon = None
    try:
        coupon_params: Dict[str, Any] = {
            "duration": "once",
            "name": f"Reduction {code}",
        }
        if type == "percent":
            coupon_params["percent_off"] = round(float(value), 2)
        else:
            coupon_params["amount_off"] = int(round(float(value) * 100))
            coupon_params["currency"] = "eur"
        coupon = stripe.Coupon.create(**coupon_params)

        pc_params: Dict[str, Any] = {"coupon": coupon.id}
        if max_redemptions:
            pc_params["max_redemptions"] = int(max_redemptions)
        if expires_at:
            try:
                exp = datetime.fromisoformat(str(expires_at))
                pc_params["expires_at"] = int(
                    exp.replace(tzinfo=exp.tzinfo or timezone.utc).timestamp()
                )
            except (TypeError, ValueError):
                logger.warning("promo %s: expires_at illisible, ignoré côté Stripe", code)
        promo_code_obj = stripe.PromotionCode.create(**pc_params)
        return coupon.id, promo_code_obj.id
    except Exception as e:
        stripe_err = getattr(stripe, "error", None) if stripe is not None else None
        if stripe_err is not None and isinstance(e, stripe_err.StripeError):
            if coupon:
                try:
                    stripe.Coupon.delete(coupon.id)
                    logger.info("coupon orphelin %s supprimé après échec Stripe", coupon.id)
                except Exception as del_err:
                    logger.error("suppression du coupon orphelin %s impossible: %s", coupon.id, del_err)
            raise StripePromoCreationError(str(e)) from e
        raise


def create_promo_code(
    code: str,
    type: str,
    value: float,
    plans: Optional[List[str]] = None,
    max_redemptions: Optional[int] = None,
    expires_at: Optional[str] = None,
) -> dict:
    """Crée un code promo localement et, si Stripe est configuré, le Coupon +
    Promotion Code Stripe associés (double plafond local ET Stripe).

    Sans Stripe, le code est créé en « local_only » : il reste visible dans
    l'admin mais sera refusé au checkout avec un message clair.
    Le doublon est vérifié SOUS le verrou : deux créations simultanées du même
    code aboutissent à un seul enregistrement.
    """
    wanted = (code or "").strip().upper()
    now_iso = datetime.now(timezone.utc).isoformat()

    with _promo_lock:
        entries = load_promo_codes()
        if _find_promo(entries, wanted):
            raise DuplicatePromoError(
                f"Le code promo « {wanted} » existe déjà."
            )

        entry: Dict[str, Any] = {
            "code": wanted,
            "active": True,
            "type": type,
            "value": float(value),
            "plans": plans or ["all"],
            "max_redemptions": int(max_redemptions) if max_redemptions else None,
            "times_used": 0,
            "counted_sessions": [],
            "expires_at": expires_at or None,
            "stripe_coupon_id": None,
            "stripe_promotion_code_id": None,
            "stripe_mode": "local_only",
            "created_at": now_iso,
            "deactivated_at": None,
        }

        if is_stripe_configured():
            coupon_id, promo_code_id = _create_stripe_refs(
                wanted, type, value, max_redemptions, expires_at
            )
            entry["stripe_coupon_id"] = coupon_id
            entry["stripe_promotion_code_id"] = promo_code_id
            entry["stripe_mode"] = stripe_mode() if stripe_mode() != "unknown" else "test"
            logger.info("promo %s créé sur Stripe (coupon=%s)", wanted, coupon_id)
        else:
            logger.info("promo %s créé en local_only (Stripe non configuré)", wanted)

        entries.append(entry)
        save_promo_codes(entries)
    return entry


def link_promo_to_stripe(code: str) -> Optional[dict]:
    """Relie un code « local_only » à Stripe : crée le Coupon + Promotion Code
    et met à jour l'enregistrement local. Renvoie None si le code est inconnu."""
    wanted = (code or "").strip().upper()
    with _promo_lock:
        entries = load_promo_codes()
        promo = _find_promo(entries, wanted)
        if not promo:
            return None
        if not promo.get("active", False):
            raise PromoValidationError(
                "inactive", f"Le code promo « {wanted} » est désactivé : réactivez-le d'abord."
            )
        if promo.get("stripe_promotion_code_id"):
            return promo  # déjà relié — rien à faire
        if not is_stripe_configured():
            raise RuntimeError("Stripe n'est pas configuré (clé secrète absente).")
        coupon_id, promo_code_id = _create_stripe_refs(
            wanted, promo.get("type", "percent"), float(promo.get("value", 0)),
            promo.get("max_redemptions"), promo.get("expires_at"),
        )
        promo["stripe_coupon_id"] = coupon_id
        promo["stripe_promotion_code_id"] = promo_code_id
        promo["stripe_mode"] = stripe_mode() if stripe_mode() != "unknown" else "test"
        save_promo_codes(entries)
    logger.info("promo %s relié à Stripe (coupon=%s)", wanted, coupon_id)
    return promo


async def create_checkout_session(
    user_id: str,
    plan: PlanType,
    billing_period: str = "monthly",  # monthly or yearly
    success_url: str = "",
    cancel_url: str = "",
    promo_code: str = "",
) -> Optional[Dict[str, Any]]:
    """Create a Stripe checkout session for subscription"""
    if not is_stripe_configured():
        return {"error": "Stripe not configured", "demo_mode": True}

    user = get_user_by_id(user_id)
    if not user:
        return {"error": "User not found"}

    plan_config = PLANS[plan]
    plan_id = plan.value

    # Réduction : validation locale AVANT Stripe (actif, forfait, expiration,
    # plafond local). Un code invalide n'entraîne aucune session Stripe.
    promo_entry: Optional[dict] = None
    promo_wanted = (promo_code or "").strip().upper()
    if promo_wanted:
        try:
            promo_entry = validate_promo_code(promo_wanted, plan_id)
        except PromoValidationError as e:
            return {"error": e.message, "error_code": "PROMO_INVALID", "promo_reason": e.reason}

    mid, yid = stripe_price_ids_for_plan(plan_id)
    price_id = mid if billing_period == "monthly" else yid

    # If no Stripe price id is configured, fall back to inline price_data.
    # This makes test-mode checkout work with only STRIPE_SECRET_KEY configured.
    line_item: Dict[str, Any]
    if price_id and price_id not in ("price_xxx", ""):
        line_item = {"price": price_id, "quantity": 1}
    else:
        amount = get_subscription_line_amount_eur(plan, billing_period)
        if amount in (None, "", -1) or float(amount) <= 0:
            return {
                "error": "Impossible de créer le checkout: tarif invalide pour ce forfait."
            }

        amount_cents = int(round(float(amount) * 100))
        interval = "year" if billing_period == "yearly" else "month"
        line_item = {
            "price_data": {
                "currency": "eur",
                "unit_amount": amount_cents,
                "recurring": {"interval": interval},
                "product_data": {
                    "name": f"Office Translator - {plan_config.get('name', plan_id)}"
                },
            },
            "quantity": 1,
        }

    
    try:
        # Create or get Stripe customer
        if user.stripe_customer_id:
            customer_id = user.stripe_customer_id
        else:
            customer = stripe.Customer.create(
                email=user.email,
                name=user.name,
                metadata={"user_id": user_id}
            )
            customer_id = customer.id
            update_user(user_id, {"stripe_customer_id": customer_id})
        
        # Create checkout session
        session_kwargs: Dict[str, Any] = {
            "customer": customer_id,
            "mode": "subscription",
            "payment_method_types": ["card"],
            "line_items": [line_item],
            "success_url": success_url or f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/checkout/success?session_id={{CHECKOUT_SESSION_ID}}",
            "cancel_url": cancel_url or f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/pricing",
            "metadata": {
                "user_id": user_id,
                "plan": plan.value,
                **({"promo_code": promo_wanted} if promo_entry else {}),
            },
            "subscription_data": {
                "metadata": {
                    "user_id": user_id,
                    "plan": plan.value,
                    **({"promo_code": promo_wanted} if promo_entry else {}),
                }
            },
        }
        if promo_entry:
            # Remise Stripe : la Promotion Code créée par l'admin porte les
            # plafonds Stripe (max_redemptions / expires_at).
            session_kwargs["discounts"] = [
                {"promotion_code": promo_entry["stripe_promotion_code_id"]}
            ]
        session = stripe.checkout.Session.create(**session_kwargs)

        return {
            "session_id": session.id,
            "url": session.url,
            **({"promo_code": promo_wanted} if promo_entry else {}),
        }
    except Exception as e:
        return {"error": str(e)}


async def sync_checkout_session(
    user_id: str,
    session_id: str,
) -> Dict[str, Any]:
    """
    Sync a completed Stripe Checkout session to local user state.
    Useful in local/dev environments where webhooks may not reach the backend.
    """
    if not is_stripe_configured():
        return {"error": "Stripe not configured"}

    user = get_user_by_id(user_id)
    if not user:
        return {"error": "User not found"}

    if not session_id or not session_id.startswith("cs_"):
        return {"error": "Invalid checkout session id"}

    try:
        session = stripe.checkout.Session.retrieve(session_id, expand=["subscription"])
    except Exception as e:
        return {"error": f"Unable to retrieve checkout session: {str(e)}"}

    metadata = session.get("metadata", {}) or {}
    session_user_id = metadata.get("user_id")
    session_customer_id = session.get("customer")

    # Security check: session must belong to current authenticated user.
    if session_user_id and session_user_id != user_id:
        return {"error": "Checkout session does not belong to current user"}
    if user.stripe_customer_id and session_customer_id and session_customer_id != user.stripe_customer_id:
        return {"error": "Checkout customer mismatch"}

    if session.get("status") != "complete":
        return {"error": "Checkout session is not completed yet"}

    await handle_checkout_completed(session)
    return {
        "status": "synced",
        "plan": metadata.get("plan"),
        "session_id": session_id,
    }


async def create_credits_checkout(
    user_id: str,
    package_index: int,
    success_url: str = "",
    cancel_url: str = "",
) -> Optional[Dict[str, Any]]:
    """Create a Stripe checkout session for credit purchase"""
    if not is_stripe_configured():
        return {"error": "Stripe not configured", "demo_mode": True}
    
    if package_index < 0 or package_index >= len(CREDIT_PACKAGES):
        return {"error": "Invalid package"}
    
    user = get_user_by_id(user_id)
    if not user:
        return {"error": "User not found"}
    
    package = CREDIT_PACKAGES[package_index]
    
    try:
        # Create or get Stripe customer
        if user.stripe_customer_id:
            customer_id = user.stripe_customer_id
        else:
            customer = stripe.Customer.create(
                email=user.email,
                name=user.name,
                metadata={"user_id": user_id}
            )
            customer_id = customer.id
            update_user(user_id, {"stripe_customer_id": customer_id})
        
        # Create checkout session
        session = stripe.checkout.Session.create(
            customer=customer_id,
            mode="payment",
            payment_method_types=["card"],
            line_items=[{"price": package["stripe_price_id"], "quantity": 1}],
            success_url=success_url or f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/dashboard?credits=purchased",
            cancel_url=cancel_url or f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/pricing",
            metadata={
                "user_id": user_id,
                "credits": package["credits"],
                "type": "credits"
            }
        )
        
        return {
            "session_id": session.id,
            "url": session.url
        }
    except Exception as e:
        return {"error": str(e)}


async def handle_webhook(payload: bytes, sig_header: str) -> Dict[str, Any]:
    """Handle Stripe webhook events"""
    if not is_stripe_configured():
        return {"error": "Stripe not configured"}
    
    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, STRIPE_WEBHOOK_SECRET
        )
    except ValueError:
        return {"error": "Invalid payload"}
    except stripe.error.SignatureVerificationError:
        return {"error": "Invalid signature"}
    
    # Handle the event
    if event["type"] == "checkout.session.completed":
        session = event["data"]["object"]
        await handle_checkout_completed(session)

    elif event["type"] == "customer.subscription.updated":
        subscription = event["data"]["object"]
        await handle_subscription_updated(subscription)

    elif event["type"] == "customer.subscription.deleted":
        subscription = event["data"]["object"]
        await handle_subscription_deleted(subscription)

    elif event["type"] == "invoice.payment_failed":
        invoice = event["data"]["object"]
        await handle_payment_failed(invoice)

    elif event["type"] == "invoice.paid":
        invoice = event["data"]["object"]
        await handle_invoice_paid(invoice)

    return {"status": "success"}


async def handle_checkout_completed(session: Dict):
    """Handle successful checkout"""
    metadata = session.get("metadata", {}) or {}
    user_id = metadata.get("user_id")
    
    if not user_id:
        return
        
    session_id = session.get("id")
    
    # Check for duplicate session processing using PaymentHistory.
    # Use the Stripe payment_intent (one-time) or subscription id (recurring) as the
    # idempotency key, NOT the checkout session id — Stripe can redeliver the event.
    db_available = False
    try:
        from database.connection import get_sync_session
        from database.models import PaymentHistory as DBPaymentHistory
        db_available = True
    except ImportError:
        pass

    payment_intent_id = session.get("payment_intent")
    subscription_id = session.get("subscription")
    if db_available and session_id:
        try:
            with get_sync_session() as db_session:
                from sqlalchemy import or_

                filters = [DBPaymentHistory.stripe_payment_intent_id == session_id]
                if payment_intent_id:
                    filters.append(DBPaymentHistory.stripe_payment_intent_id == payment_intent_id)
                if subscription_id:
                    filters.append(DBPaymentHistory.stripe_invoice_id == subscription_id)
                existing = db_session.query(DBPaymentHistory).filter(or_(*filters)).first()
                if existing:
                    logger.info("Checkout session %s already processed (pi=%s sub=%s). Skipping.",
                                session_id, payment_intent_id, subscription_id)
                    return
        except Exception as e:
            logger.error("Error checking PaymentHistory duplication: %s", e)

    user = get_user_by_id(user_id)
    if not user:
        return

    # Check if it's a credit purchase
    if metadata.get("type") == "credits":
        credits = int(metadata.get("credits", 0))
        add_credits(user_id, credits)
        
        # Log to PaymentHistory
        if db_available and session_id:
            try:
                with get_sync_session() as db_session:
                    payment = DBPaymentHistory(
                        user_id=user_id,
                        stripe_payment_intent_id=payment_intent_id or session_id,
                        stripe_invoice_id=session.get("invoice") or session.get("subscription"),
                        amount_cents=session.get("amount_total") or 0,
                        currency=session.get("currency") or "usd",
                        payment_type="credits",
                        status="succeeded",
                        description=f"Achat de {credits} crédits",
                    )
                    db_session.add(payment)
                    db_session.commit()
            except Exception as e:
                logger.error("Failed to write credits payment to history: %s", e)

        # Send Email
        try:
            from services.email_service import send_subscription_email_async
            await send_subscription_email_async(
                to_email=user.email,
                user_name=user.name,
                event_type="credits_purchased",
                details={"credits": credits}
            )
        except Exception as e:
            logger.error("Failed to send credit purchase email: %s", e)

        # Send Telegram notification
        try:
            from utils.telegram import send_telegram_notification
            import asyncio
            msg = (
                f"💰 *Achat de crédits !*\n\n"
                f"• *Utilisateur* : `{user.name or 'Non renseigné'}` (`{user.email}`)\n"
                f"• *Crédits achetés* : `{credits}`\n"
                f"• *Montant* : `{session.get('amount_total', 0) / 100:.2f} {session.get('currency', 'eur').upper()}`\n"
                f"• *ID Transaction* : `{session_id}`"
            )
            asyncio.create_task(send_telegram_notification(msg))
        except Exception as tel_err:
            logger.error(f"Failed to send telegram notification for credits checkout: {tel_err}")
        return
    
    # It's a subscription
    plan = metadata.get("plan")
    if plan:
        subscription_raw = session.get("subscription")
        subscription_id = None
        subscription_ends_at = None

        if isinstance(subscription_raw, str):
            # Not expanded — fetch subscription to get period end
            try:
                sub = stripe.Subscription.retrieve(subscription_raw)
                subscription_id = sub["id"]
                if sub.get("current_period_end"):
                    from datetime import timezone
                    subscription_ends_at = datetime.fromtimestamp(
                        sub["current_period_end"], tz=timezone.utc
                    )
            except Exception:
                subscription_id = subscription_raw
        elif subscription_raw:
            # Expanded subscription object (from sync path)
            subscription_id = subscription_raw.get("id")
            period_end = subscription_raw.get("current_period_end")
            if period_end:
                subscription_ends_at = datetime.fromtimestamp(period_end, tz=timezone.utc)

        # Derive tier from plan (DB constraint: only 'free' or 'pro')
        tier = "pro" if plan in ("pro", "business", "enterprise") else "free"

        is_new_sub = (
            user.stripe_subscription_id != subscription_id
            or user.subscription_status != SubscriptionStatus.ACTIVE.value
            or user.plan != plan
        )

        update_user(user_id, {
            "plan": plan,
            "tier": tier,
            "subscription_status": SubscriptionStatus.ACTIVE.value,
            "stripe_subscription_id": subscription_id,
            "stripe_customer_id": session.get("customer") or None,
            "subscription_ends_at": subscription_ends_at,
            "cancel_at_period_end": False,
            "docs_translated_this_month": 0,
            "pages_translated_this_month": 0,
        })
        logger.info("Checkout synced: user %s → plan=%s tier=%s sub=%s", user_id, plan, tier, subscription_id)

        # Réduction : le compteur local du code promo n'augmente qu'à la
        # complétion réelle du paiement (webhook OU synchronisation manuelle),
        # via la métadonnée posée à la création de la session. Les crédits ne
        # supportent pas de code promo.
        promo_used = (metadata.get("promo_code") or "").strip()
        if promo_used:
            try:
                increment_promo_usage(promo_used, session_id=session_id or "")
            except Exception as promo_err:
                logger.error("promo usage increment failed for %s: %s", promo_used, promo_err)

        # Log to PaymentHistory
        if db_available and session_id:
            try:
                with get_sync_session() as db_session:
                    payment = DBPaymentHistory(
                        user_id=user_id,
                        stripe_payment_intent_id=payment_intent_id or session_id,
                        stripe_invoice_id=subscription_id or session.get("invoice"),
                        amount_cents=session.get("amount_total") or 0,
                        currency=session.get("currency") or "usd",
                        payment_type="subscription",
                        status="succeeded",
                        description=f"Abonnement au forfait {plan}",
                    )
                    db_session.add(payment)
                    db_session.commit()
            except Exception as e:
                logger.error("Failed to write subscription payment to history: %s", e)

        # Send activation email
        if is_new_sub:
            try:
                from services.email_service import send_subscription_email_async
                ends_str = subscription_ends_at.strftime("%d/%m/%Y") if subscription_ends_at else ""
                await send_subscription_email_async(
                    to_email=user.email,
                    user_name=user.name,
                    event_type="activated",
                    details={"plan_name": plan, "ends_at": ends_str}
                )
            except Exception as e:
                logger.error("Failed to send activation email: %s", e)

        # Send Telegram notification
        try:
            from utils.telegram import send_telegram_notification
            import asyncio
            ends_str = subscription_ends_at.strftime("%d/%m/%Y") if subscription_ends_at else "Non spécifiée"
            msg = (
                f"⭐ *Nouvel Abonnement Activé !*\n\n"
                f"• *Utilisateur* : `{user.name or 'Non renseigné'}` (`{user.email}`)\n"
                f"• *Forfait* : `{plan.upper()}`\n"
                f"• *Montant* : `{session.get('amount_total', 0) / 100:.2f} {session.get('currency', 'eur').upper()}`\n"
                f"• *Date de fin* : {ends_str}"
            )
            asyncio.create_task(send_telegram_notification(msg))
        except Exception as tel_err:
            logger.error(f"Failed to send telegram notification for subscription activation: {tel_err}")


async def handle_subscription_updated(subscription: Dict):
    """Handle subscription updates"""
    metadata = subscription.get("metadata", {}) or {}
    user_id = metadata.get("user_id")
    
    if not user_id:
        return
        
    user = get_user_by_id(user_id)
    if not user:
        return
    
    status_map = {
        "active": SubscriptionStatus.ACTIVE,
        "past_due": SubscriptionStatus.PAST_DUE,
        "canceled": SubscriptionStatus.CANCELED,
        "trialing": SubscriptionStatus.TRIALING,
        "paused": SubscriptionStatus.PAUSED,
    }
    
    stripe_status = subscription.get("status", "active")
    status = status_map.get(stripe_status, SubscriptionStatus.ACTIVE)
    
    stripe_cancel_at_period_end = subscription.get("cancel_at_period_end", False)
    is_newly_cancelling = stripe_cancel_at_period_end and not user.cancel_at_period_end

    period_end = subscription.get("current_period_end")
    ends_str = ""
    if period_end:
        ends_str = datetime.fromtimestamp(period_end, tz=timezone.utc).strftime("%d/%m/%Y")

    period_end = subscription.get("current_period_end")
    update_user(user_id, {
        "subscription_status": status.value,
        "cancel_at_period_end": stripe_cancel_at_period_end,
        "subscription_ends_at": datetime.fromtimestamp(period_end, tz=timezone.utc) if period_end else None,
    })

    # Send cancellation email if they just selected to cancel
    if is_newly_cancelling:
        try:
            from services.email_service import send_subscription_email_async
            await send_subscription_email_async(
                to_email=user.email,
                user_name=user.name,
                event_type="cancelled",
                details={"ends_at": ends_str}
            )
        except Exception as e:
            logger.error("Failed to send cancellation email in handle_subscription_updated: %s", e)

        # Send Telegram notification for cancellation request
        try:
            from utils.telegram import send_telegram_notification
            import asyncio
            msg = (
                f"🔕 *Désinscription programmée (Stripe)*\n\n"
                f"• *Utilisateur* : `{user.name or 'Non renseigné'}` (`{user.email}`)\n"
                f"• *Date de fin d'accès* : {ends_str}"
            )
            asyncio.create_task(send_telegram_notification(msg))
        except Exception as tel_err:
            logger.error(f"Failed to send telegram notification for subscription cancellation request: {tel_err}")


async def handle_subscription_deleted(subscription: Dict):
    """Handle subscription cancellation (actually ended)"""
    metadata = subscription.get("metadata", {}) or {}
    user_id = metadata.get("user_id")
    
    if not user_id:
        return
        
    user = get_user_by_id(user_id)
    if not user:
        return

    had_active_sub = user.plan != PlanType.FREE or user.tier != "free"

    update_user(user_id, {
        "plan": PlanType.FREE.value,
        "tier": "free",
        "subscription_status": SubscriptionStatus.CANCELED.value,
        "stripe_subscription_id": None,
    })

    # Send ended email
    if had_active_sub:
        try:
            from services.email_service import send_subscription_email_async
            await send_subscription_email_async(
                to_email=user.email,
                user_name=user.name,
                event_type="ended",
                details={}
            )
        except Exception as e:
            logger.error("Failed to send subscription ended email: %s", e)

        # Send Telegram notification for ended subscription
        try:
            from utils.telegram import send_telegram_notification
            import asyncio
            msg = (
                f"❌ *Abonnement Terminé / Expiré (Stripe)*\n\n"
                f"• *Utilisateur* : `{user.name or 'Non renseigné'}` (`{user.email}`)\n"
                f"• *Statut* : Retour au forfait `FREE`"
            )
            asyncio.create_task(send_telegram_notification(msg))
        except Exception as tel_err:
            logger.error(f"Failed to send telegram notification for subscription ended: {tel_err}")


async def handle_payment_failed(invoice: Dict):
    """Handle failed payment — set subscription status to PAST_DUE and send email"""
    customer_id = invoice.get("customer")
    if not customer_id:
        return

    # Find user by stripe_customer_id
    try:
        from database.connection import get_sync_session
        from database.models import User as DBUser

        with get_sync_session() as session:
            db_user = (
                session.query(DBUser)
                .filter(DBUser.stripe_customer_id == customer_id)
                .first()
            )
            if db_user:
                user_id = str(db_user.id)
                db_user.subscription_status = SubscriptionStatus.PAST_DUE
                session.commit()
                logger.warning(
                    "Payment failed for customer %s (user %s) — status set to past_due",
                    customer_id, user_id,
                )

                # Send email
                try:
                    from services.email_service import send_subscription_email_async
                    await send_subscription_email_async(
                        to_email=db_user.email,
                        user_name=db_user.name,
                        event_type="payment_failed",
                        details={}
                    )
                except Exception as e:
                    logger.error("Failed to send payment failed email: %s", e)

                # Send Telegram notification
                try:
                    from utils.telegram import send_telegram_notification
                    import asyncio
                    msg = (
                        f"⚠️ *Échec de paiement (Stripe)*\n\n"
                        f"• *Utilisateur* : `{db_user.name or 'Non renseigné'}` (`{db_user.email}`)\n"
                        f"• *Statut* : Facture impayée, forfait passé en `PAST_DUE`"
                    )
                    asyncio.create_task(send_telegram_notification(msg))
                except Exception as tel_err:
                    logger.error(f"Failed to send telegram notification for payment failed: {tel_err}")
    except Exception as exc:
        logger.error("handle_payment_failed DB error: %s", exc)


async def handle_invoice_paid(invoice: Dict):
    """Extend subscription_ends_at when a recurring invoice is paid."""
    customer_id = invoice.get("customer")
    if not customer_id:
        return

    subscription_id = invoice.get("subscription")
    period_end = invoice.get("period_end") or invoice.get("lines", {}).get("data", [{}])[0].get("period", {}).get("end")

    try:
        from database.connection import get_sync_session
        from database.models import User as DBUser

        with get_sync_session() as session:
            db_user = (
                session.query(DBUser)
                .filter(DBUser.stripe_customer_id == customer_id)
                .first()
            )
            if not db_user:
                return

            if subscription_id and db_user.stripe_subscription_id != subscription_id:
                # The paid invoice belongs to a different subscription; do not update.
                logger.warning(
                    "Invoice paid for customer %s but subscription id mismatch (expected %s, got %s)",
                    customer_id, db_user.stripe_subscription_id, subscription_id,
                )
                return

            if period_end:
                new_end = datetime.fromtimestamp(period_end, tz=timezone.utc)
                if db_user.subscription_ends_at is None or new_end > db_user.subscription_ends_at:
                    db_user.subscription_ends_at = new_end
                    db_user.updated_at = datetime.now(timezone.utc)
                    session.commit()
                    logger.info(
                        "Extended subscription_ends_at for user %s to %s",
                        db_user.id, new_end.isoformat(),
                    )
    except Exception as exc:
        logger.error("handle_invoice_paid error: %s", exc)


async def cancel_subscription(user_id: str) -> Dict[str, Any]:
    """Cancel a user's subscription at period end."""
    if not is_stripe_configured():
        return {"error": "Stripe not configured"}

    user = get_user_by_id(user_id)
    if not user or not user.stripe_subscription_id:
        return {"error": "No active subscription found"}

    try:
        subscription = stripe.Subscription.retrieve(user.stripe_subscription_id)
        if subscription.customer != user.stripe_customer_id:
            return {"error": "Subscription does not belong to current user"}

        subscription = stripe.Subscription.modify(
            user.stripe_subscription_id,
            cancel_at_period_end=True,
        )

        cancel_at = None
        if subscription.cancel_at:
            cancel_at = datetime.fromtimestamp(subscription.cancel_at, tz=timezone.utc)

        subscription_ends_at = None
        ends_str = ""
        if subscription.current_period_end:
            subscription_ends_at = datetime.fromtimestamp(subscription.current_period_end, tz=timezone.utc)
            ends_str = datetime.fromtimestamp(subscription.current_period_end, tz=timezone.utc).strftime("%d/%m/%Y")

        is_new_cancel = not user.cancel_at_period_end

        update_user(user_id, {
            "cancel_at_period_end": True,
            "subscription_ends_at": subscription_ends_at,
        })

        # Send cancellation confirmation email
        if is_new_cancel:
            try:
                from services.email_service import send_subscription_email_async
                await send_subscription_email_async(
                    to_email=user.email,
                    user_name=user.name,
                    event_type="cancelled",
                    details={"ends_at": ends_str}
                )
            except Exception as e:
                logger.error("Failed to send cancellation email in cancel_subscription: %s", e)

        return {
            "status": "canceling",
            "cancel_at": cancel_at,
            "subscription_ends_at": subscription_ends_at,
        }
    except Exception as e:
        return {"error": str(e)}


# ─── cached portal config id (created once per process) ───────────────────────
_PORTAL_CONFIG_ID: Optional[str] = None


def _get_or_create_portal_config() -> Optional[str]:
    """
    Return a Stripe Customer Portal configuration ID.
    Creates one programmatically if none exists yet, so no Stripe Dashboard
    manual setup is required.
    """
    global _PORTAL_CONFIG_ID
    if _PORTAL_CONFIG_ID:
        return _PORTAL_CONFIG_ID

    try:
        # Re-use the first existing configuration if there is one
        existing = stripe.billing_portal.Configuration.list(active=True, limit=1)
        if existing.data:
            _PORTAL_CONFIG_ID = existing.data[0].id
            return _PORTAL_CONFIG_ID

        # Create a minimal configuration programmatically
        config = stripe.billing_portal.Configuration.create(
            business_profile={
                "headline": "Wordly.art — Gérer mon abonnement",
            },
            features={
                "invoice_history": {"enabled": True},
                "payment_method_update": {"enabled": True},
                "subscription_cancel": {
                    "enabled": True,
                    "mode": "at_period_end",   # keeps access until end of paid period
                    "proration_behavior": "none",
                    "cancellation_reason": {
                        "enabled": True,
                        "options": [
                            "too_expensive",
                            "missing_features",
                            "switched_service",
                            "unused",
                            "other",
                        ],
                    },
                },
                "customer_update": {
                    "enabled": True,
                    "allowed_updates": ["email", "name", "address"],
                },
            },
        )
        _PORTAL_CONFIG_ID = config.id
        logger.info("Stripe portal configuration created: %s", _PORTAL_CONFIG_ID)
        return _PORTAL_CONFIG_ID

    except Exception as e:
        logger.error("Failed to get/create Stripe portal config: %s", e)
        return None


async def get_billing_portal_url(user_id: str) -> Optional[str]:
    """Get Stripe billing portal URL for customer"""
    if not is_stripe_configured():
        return None

    user = get_user_by_id(user_id)
    if not user or not user.stripe_customer_id:
        return None

    try:
        config_id = _get_or_create_portal_config()
        kwargs: Dict[str, Any] = {
            "customer": user.stripe_customer_id,
            "return_url": (
                f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}"
                "/dashboard/profile?tab=subscription"
            ),
        }
        if config_id:
            kwargs["configuration"] = config_id

        session = stripe.billing_portal.Session.create(**kwargs)
        return session.url
    except Exception as e:
        logger.error("get_billing_portal_url error: %s", e)
        return None

