"""Stripe webhook handler (B-2.8) — subscription lifecycle.

Endpoint: POST /checkout/webhooks/stripe (CSRF-exempt; signature verified)

Handled events:
- checkout.session.completed:        create Subscription row, set User.subscription_status
- customer.subscription.updated:     update status + current_period_end
- customer.subscription.deleted:     mark status='canceled', clear User.subscription_status
- invoice.payment_succeeded:         touch updated_at (renewal confirmation)
- invoice.payment_failed:            set status='past_due'

All other event types are acknowledged with 200 but ignored.

Idempotency: every processed event ID is recorded in WebhookEvent.
Duplicate deliveries return 200 immediately without re-processing.
"""
import logging
import os
from datetime import datetime, timezone
from typing import Optional

import stripe
from flask import jsonify, request

from src.billing import billing_bp
from src.billing.plans import VALID_PERIODS, get_plan
from src.billing.stripe_client import is_stripe_configured
from src.database import db
from src.models import Subscription, User, WebhookEvent

logger = logging.getLogger(__name__)


def get_webhook_secret() -> Optional[str]:
    """Return STRIPE_WEBHOOK_SECRET, or None if not configured."""
    return os.environ.get('STRIPE_WEBHOOK_SECRET')


def is_webhook_configured() -> bool:
    return bool(get_webhook_secret() and is_stripe_configured())


def _verify_event(payload: bytes, sig_header: str):
    """Validate Stripe signature and return the parsed event, or None on failure."""
    secret = get_webhook_secret()
    if not secret:
        logger.error('STRIPE_WEBHOOK_SECRET not set; rejecting webhook')
        return None
    try:
        return stripe.Webhook.construct_event(payload, sig_header, secret)
    except ValueError:
        logger.warning('Stripe webhook: invalid JSON payload')
        return None
    except stripe.error.SignatureVerificationError:
        logger.warning('Stripe webhook: signature verification failed')
        return None


def _is_duplicate(event_id: str) -> bool:
    return WebhookEvent.query.filter_by(stripe_event_id=event_id).first() is not None


def _resolve_user_for_event(event_obj: dict) -> Optional[User]:
    """Resolve the DictIA User from a Stripe event object.

    Trust order (anti-tamper per B-2.7 review note):
    1. Look up by stripe_customer_id on the event object — this is server-set
       by Stripe at customer creation, not user-controlled.
    2. Fall back to event metadata 'dictia_user_id', re-validated against DB.
    3. Fall back to customer_email lookup (last resort, rare for subscriptions).
    """
    cust_id = event_obj.get('customer')
    if cust_id:
        user = User.query.filter_by(stripe_customer_id=cust_id).first()
        if user:
            return user

    metadata = event_obj.get('metadata') or {}
    raw_user_id = metadata.get('dictia_user_id')
    if raw_user_id:
        try:
            uid = int(raw_user_id)
        except (TypeError, ValueError):
            uid = None
        if uid is not None:
            user = db.session.get(User, uid)
            if user:
                # Bind stripe_customer_id if missing (defensive)
                if not user.stripe_customer_id and cust_id:
                    user.stripe_customer_id = cust_id
                return user

    email = event_obj.get('customer_email')
    if email:
        user = User.query.filter_by(email=email.lower().strip()).first()
        if user and cust_id and not user.stripe_customer_id:
            user.stripe_customer_id = cust_id
        return user

    return None


def _resolve_plan_period(event_obj: dict, default_period: str = 'monthly') -> tuple:
    """Extract plan_slug and period from event metadata, validating both."""
    metadata = event_obj.get('metadata') or {}
    plan_slug = metadata.get('dictia_plan_slug')
    period = metadata.get('dictia_period', default_period)
    if get_plan(plan_slug) is None:
        plan_slug = None  # invalid / missing — leave for handler to log
    if period not in VALID_PERIODS:
        period = default_period
    return plan_slug, period


def _ts_to_dt(ts) -> Optional[datetime]:
    if ts is None:
        return None
    try:
        return datetime.fromtimestamp(int(ts), tz=timezone.utc).replace(tzinfo=None)
    except (TypeError, ValueError, OSError):
        return None


def _record_event(event, sub_id: Optional[str], cust_id: Optional[str]) -> None:
    """Insert a WebhookEvent row marking this event as processed."""
    db.session.add(WebhookEvent(
        stripe_event_id=event.id,
        event_type=event.type,
        stripe_subscription_id=sub_id,
        stripe_customer_id=cust_id,
    ))


def _handle_checkout_session_completed(event) -> None:
    obj = event.data.object  # stripe.checkout.Session
    user = _resolve_user_for_event(obj)
    sub_id = obj.get('subscription')
    cust_id = obj.get('customer')
    plan_slug, period = _resolve_plan_period(obj)

    if not user:
        logger.warning('checkout.session.completed: no user for cust=%s sub=%s', cust_id, sub_id)
        _record_event(event, sub_id, cust_id)
        return
    if not sub_id:
        logger.warning('checkout.session.completed: missing subscription id for user %s', user.id)
        _record_event(event, sub_id, cust_id)
        return
    if not plan_slug:
        logger.warning('checkout.session.completed: missing/invalid plan_slug metadata for sub=%s', sub_id)
        plan_slug = 'unknown'

    # Look up the existing subscription row (defensive against duplicate webhooks)
    existing = Subscription.query.filter_by(stripe_subscription_id=sub_id).first()
    now = datetime.utcnow()
    if existing:
        existing.status = 'active'
        existing.updated_at = now
    else:
        # We need current_period_end — pull it from the subscription object
        # if the event includes it; otherwise leave None and let
        # customer.subscription.updated fill it in.
        period_end = None
        # Fetch the subscription via Stripe API for accurate period_end
        try:
            from src.billing.stripe_client import _ensure_configured
            _ensure_configured()
            sub_obj = stripe.Subscription.retrieve(sub_id)
            period_end = _ts_to_dt(sub_obj.get('current_period_end'))
        except Exception as e:
            logger.warning('Could not fetch subscription %s for period_end: %s', sub_id, e)
        db.session.add(Subscription(
            user_id=user.id,
            stripe_customer_id=cust_id,
            stripe_subscription_id=sub_id,
            plan_slug=plan_slug,
            period=period,
            status='active',
            current_period_end=period_end,
            created_at=now,
            updated_at=now,
        ))

    user.subscription_status = 'active'
    if cust_id and not user.stripe_customer_id:
        user.stripe_customer_id = cust_id
    _record_event(event, sub_id, cust_id)


def _handle_subscription_updated(event) -> None:
    obj = event.data.object  # stripe.Subscription
    sub_id = obj.get('id')
    cust_id = obj.get('customer')
    new_status = obj.get('status')
    period_end = _ts_to_dt(obj.get('current_period_end'))

    sub = Subscription.query.filter_by(stripe_subscription_id=sub_id).first()
    user = _resolve_user_for_event({'customer': cust_id, 'metadata': obj.get('metadata') or {}})

    now = datetime.utcnow()
    if sub:
        if new_status:
            sub.status = new_status
        if period_end:
            sub.current_period_end = period_end
        sub.updated_at = now
    else:
        # Webhook arrived before we created the row (race) — create defensively
        plan_slug, period = _resolve_plan_period(obj)
        db.session.add(Subscription(
            user_id=user.id if user else None,
            stripe_customer_id=cust_id,
            stripe_subscription_id=sub_id,
            plan_slug=plan_slug or 'unknown',
            period=period,
            status=new_status or 'unknown',
            current_period_end=period_end,
            created_at=now,
            updated_at=now,
        ))

    if user and new_status:
        user.subscription_status = new_status
    _record_event(event, sub_id, cust_id)


def _handle_subscription_deleted(event) -> None:
    obj = event.data.object
    sub_id = obj.get('id')
    cust_id = obj.get('customer')
    sub = Subscription.query.filter_by(stripe_subscription_id=sub_id).first()
    user = _resolve_user_for_event({'customer': cust_id, 'metadata': obj.get('metadata') or {}})
    now = datetime.utcnow()
    if sub:
        sub.status = 'canceled'
        sub.updated_at = now
    if user:
        user.subscription_status = 'canceled'
    _record_event(event, sub_id, cust_id)


def _handle_invoice_payment_succeeded(event) -> None:
    obj = event.data.object  # stripe.Invoice
    sub_id = obj.get('subscription')
    cust_id = obj.get('customer')
    if sub_id:
        sub = Subscription.query.filter_by(stripe_subscription_id=sub_id).first()
        if sub:
            sub.updated_at = datetime.utcnow()
            if sub.status == 'past_due':
                sub.status = 'active'
                user = _resolve_user_for_event({'customer': cust_id, 'metadata': {}})
                if user:
                    user.subscription_status = 'active'
    _record_event(event, sub_id, cust_id)


def _handle_invoice_payment_failed(event) -> None:
    obj = event.data.object
    sub_id = obj.get('subscription')
    cust_id = obj.get('customer')
    if sub_id:
        sub = Subscription.query.filter_by(stripe_subscription_id=sub_id).first()
        user = _resolve_user_for_event({'customer': cust_id, 'metadata': {}})
        if sub:
            sub.status = 'past_due'
            sub.updated_at = datetime.utcnow()
        if user:
            user.subscription_status = 'past_due'
    _record_event(event, sub_id, cust_id)


_HANDLERS = {
    'checkout.session.completed': _handle_checkout_session_completed,
    'customer.subscription.updated': _handle_subscription_updated,
    'customer.subscription.deleted': _handle_subscription_deleted,
    'invoice.payment_succeeded': _handle_invoice_payment_succeeded,
    'invoice.payment_failed': _handle_invoice_payment_failed,
}


@billing_bp.route('/webhooks/stripe', methods=['POST'])
def stripe_webhook():
    """Stripe webhook endpoint. Signature-verified; CSRF-exempt.

    Returns 400 on signature failure (Stripe will retry); 200 otherwise
    (even for unhandled event types, to acknowledge receipt).
    """
    payload = request.get_data()
    sig_header = request.headers.get('Stripe-Signature', '')
    event = _verify_event(payload, sig_header)
    if event is None:
        return jsonify({'error': 'invalid_signature'}), 400

    # Idempotency check
    if _is_duplicate(event.id):
        logger.info('Stripe webhook: duplicate event %s ignored', event.id)
        return jsonify({'received': True, 'duplicate': True})

    handler = _HANDLERS.get(event.type)
    if handler is None:
        # Unhandled event type — record + ack so Stripe stops retrying
        _record_event(event, None, None)
        try:
            db.session.commit()
        except Exception:
            db.session.rollback()
        return jsonify({'received': True, 'handled': False})

    try:
        handler(event)
        db.session.commit()
    except Exception as e:
        logger.exception('Stripe webhook: handler for %s failed: %s', event.type, e)
        db.session.rollback()
        # Return 500 so Stripe retries — but only for genuine handler failures,
        # not for malformed/unhandled events
        return jsonify({'error': 'handler_failed'}), 500

    return jsonify({'received': True})
