"""Payment service — provider-agnostic orchestration.

The single business-logic layer for payments. It:

* creates a gateway order from a **server-resolved** plan price (never trusts a
  browser-supplied amount) and records a pending :class:`Payment`;
* verifies a Checkout callback signature and, only then, captures the payment and
  activates premium — reusing the existing Premium activation service;
* processes gateway **webhooks** as the authoritative source of truth, idempotently.

All gateway calls go through :mod:`app.payments.razorpay_service`; nothing here
imports the Razorpay SDK. Premium activation goes through
:func:`app.premium.services.activate_premium_from_payment`, so subscription logic
and notifications are never duplicated.
"""

from __future__ import annotations

import json
import uuid
from decimal import Decimal

from flask import current_app

from app.extensions import db
from app.models.base import utcnow
from app.models.enums import PaymentStatus
from app.payments import razorpay_service
from app.payments.models import Payment
from app.payments.razorpay_service import PROVIDER, SignatureError


class PaymentError(Exception):
    """Base error for the payment workflow."""


class PlanUnavailableError(PaymentError):
    """Raised when a plan is unknown, disabled, or has no online price."""


class PaymentNotFoundError(PaymentError):
    """Raised when a referenced payment/order cannot be resolved for the user."""


# --- helpers ---------------------------------------------------------------

def _get_purchasable_plan(plan_name: str):
    from app.premium.config import get_plan_by_name

    plan = get_plan_by_name(plan_name)
    if plan is None or not plan.enabled:
        raise PlanUnavailableError("That premium plan is not available.")
    if Decimal(str(plan.price)) <= 0:
        raise PlanUnavailableError("This plan cannot be purchased online.")
    return plan


def _payment_by_order(order_id: str) -> Payment | None:
    if not order_id:
        return None
    return db.session.scalar(
        db.select(Payment).where(Payment.provider_order_id == order_id)
    )


def _payment_by_provider_payment_id(payment_id: str) -> Payment | None:
    if not payment_id:
        return None
    return db.session.scalar(
        db.select(Payment).where(Payment.provider_payment_id == payment_id)
    )


def _merge_notes(payment: Payment, **extra) -> None:
    """Merge ``extra`` into the payment's JSON notes (best-effort, never raises)."""
    try:
        data = json.loads(payment.notes) if payment.notes else {}
    except (TypeError, ValueError):
        data = {}
    data.update({k: v for k, v in extra.items() if v is not None})
    payment.notes = json.dumps(data)


def _notify(user, title: str, message: str) -> None:
    """Reuse the existing premium notification helper (never breaks the flow)."""
    from app.premium.services import _notify_premium

    _notify_premium(user, title, message)


# --- order creation --------------------------------------------------------

def create_premium_order(user, plan_name: str) -> dict:
    """Create a gateway order for ``plan_name`` and record a pending payment.

    The amount is derived from the admin-configured plan price on the server; the
    browser only receives the public key, order id and amount to open Checkout.
    Returns the context the frontend needs — never any secret.
    """
    plan = _get_purchasable_plan(plan_name)
    price = Decimal(str(plan.price))
    currency = current_app.config.get("PREMIUM_CURRENCY", "INR")
    amount_minor = int((price * 100).to_integral_value())
    receipt = "prem_" + uuid.uuid4().hex[:20]
    notes = {
        "user_public_id": user.public_id,
        "plan": plan.name,
        "purpose": "premium_subscription",
    }

    order = razorpay_service.create_order(amount_minor, currency, receipt, notes)

    payment = Payment(
        user_id=user.id,
        plan_name=plan.name,
        provider=PROVIDER,
        provider_order_id=order["id"],
        amount=price,
        currency=currency,
        status=PaymentStatus.CREATED,
        notes=json.dumps({"receipt": receipt}),
    )
    db.session.add(payment)
    db.session.commit()

    profile = getattr(user, "profile", None)
    return {
        "key_id": current_app.config.get("RAZORPAY_KEY_ID", ""),
        "order_id": order["id"],
        "amount": amount_minor,
        "currency": currency,
        "plan": plan.name,
        "payment_public_id": payment.public_id,
        "name": getattr(profile, "full_name", None) or user.email,
        "email": user.email,
    }


# --- capture + activation --------------------------------------------------

def _capture_and_activate(payment: Payment, payment_id: str | None = None,
                          signature: str | None = None) -> Payment:
    """Mark a payment captured and activate premium — idempotent.

    Guarded on ``CAPTURED`` so a duplicate callback/webhook can never activate
    premium twice or create a second subscription.
    """
    if payment.status == PaymentStatus.CAPTURED:
        return payment

    payment.status = PaymentStatus.CAPTURED
    if payment_id:
        payment.provider_payment_id = payment_id
    if signature:
        payment.provider_signature = signature
    payment.paid_at = utcnow()
    db.session.commit()

    # Reuse the Premium activation service (subscription + "Premium Activated"
    # notification + activity feed). Never duplicated here.
    from app.premium.services import activate_premium_from_payment

    subscription = activate_premium_from_payment(payment.user, payment.plan_name)
    payment.subscription_id = subscription.id
    db.session.commit()

    _notify(payment.user, "Payment Successful",
            f"We received your payment for the {payment.plan_name} plan. "
            "Premium is now active on your account.")
    return payment


def _mark_failed(payment: Payment, payment_id: str | None = None,
                 reason: str | None = None, *, notify: bool = True) -> Payment:
    """Mark a payment failed (never downgrades a captured payment)."""
    if payment.status in (PaymentStatus.CAPTURED, PaymentStatus.REFUNDED):
        return payment
    if payment.status == PaymentStatus.FAILED:
        return payment
    payment.status = PaymentStatus.FAILED
    if payment_id:
        payment.provider_payment_id = payment_id
    _merge_notes(payment, failure_reason=reason)
    db.session.commit()
    if notify:
        _notify(payment.user, "Payment Failed",
                f"Your payment for the {payment.plan_name} plan did not complete. "
                "No charge was made — you can try again anytime.")
    return payment


def verify_callback(user, order_id: str, payment_id: str, signature: str) -> Payment:
    """Verify a Checkout callback and activate premium on success.

    Premium is never activated before signature verification succeeds. Idempotent:
    a repeated callback for an already-captured order is a no-op.
    """
    payment = _payment_by_order(order_id)
    if payment is None or payment.user_id != getattr(user, "id", None):
        raise PaymentNotFoundError("No matching pending payment was found.")
    if payment.status == PaymentStatus.CAPTURED:
        return payment

    if not razorpay_service.verify_payment_signature(order_id, payment_id, signature):
        _mark_failed(payment, payment_id, reason="signature_verification_failed")
        raise SignatureError("Payment could not be verified.")

    return _capture_and_activate(payment, payment_id, signature)


# --- webhooks (authoritative, idempotent) ----------------------------------

def _entity(payload: dict, kind: str) -> dict:
    """Safely pull ``payload.payload.<kind>.entity`` from a webhook body."""
    try:
        return payload["payload"][kind]["entity"] or {}
    except (KeyError, TypeError):
        return {}


def handle_webhook_event(event_type: str, payload: dict) -> str:
    """Process a *verified* webhook event idempotently. Returns a status string.

    Webhooks are treated as the authoritative source of payment status: a capture
    activates premium even if the browser callback never arrived, and repeated
    deliveries never double-activate (guarded by payment status).
    """
    if event_type == "payment.captured":
        entity = _entity(payload, "payment")
        payment = _payment_by_order(entity.get("order_id"))
        if payment is None:
            return "ignored"
        _capture_and_activate(payment, entity.get("id"))
        return "captured"

    if event_type == "payment.failed":
        entity = _entity(payload, "payment")
        payment = _payment_by_order(entity.get("order_id"))
        if payment is None:
            return "ignored"
        _mark_failed(payment, entity.get("id"),
                     reason=(entity.get("error_description") or "payment_failed"))
        return "failed"

    if event_type in ("refund.processed", "refund.created"):
        entity = _entity(payload, "refund")
        payment = (
            _payment_by_provider_payment_id(entity.get("payment_id"))
            or _payment_by_order(_entity(payload, "payment").get("order_id"))
        )
        if payment is None:
            return "ignored"
        _process_refund(payment, entity.get("id"))
        return "refunded"

    return "ignored"


def get_user_payments(user) -> list[Payment]:
    """A user's own payments, newest first (for their payment-history page)."""
    return list(db.session.scalars(
        db.select(Payment)
        .where(Payment.user_id == user.id)
        .order_by(Payment.created_at.desc())
    ))


def get_user_payment_or_404(public_id: str, user) -> Payment:
    """Fetch one of the user's own payments by public id, or 404.

    Enforces ownership so a user can only ever view their own payment/receipt.
    """
    payment = db.session.scalar(
        db.select(Payment).where(
            Payment.public_id == public_id, Payment.user_id == user.id
        )
    )
    if payment is None:
        from flask import abort
        abort(404)
    return payment


def _process_refund(payment: Payment, refund_id: str | None = None) -> Payment:
    """Mark a payment refunded, revoke premium, and notify — idempotent."""
    if payment.status == PaymentStatus.REFUNDED:
        return payment
    payment.status = PaymentStatus.REFUNDED
    _merge_notes(payment, refund_id=refund_id)
    db.session.commit()

    # A refunded purchase should not retain premium. Reuse the premium expiry
    # service (keeps subscription history; entitlement drops immediately).
    from app.premium.services import expire_premium

    expire_premium(payment.user)
    _notify(payment.user, "Refund Processed",
            f"Your payment for the {payment.plan_name} plan has been refunded. "
            "Premium access has been removed.")
    return payment
