"""Subscription services — write operations.

All mutation functions live here. Each takes explicit arguments to avoid
coupling to HTTP request objects, making them fully testable in isolation.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from django.db import transaction
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from simorgh.apps.subscription.models import (
    PlatformPlan,
    SubscriptionStatus,
    TenantAddonFeature,
    TenantSubscription,
)

if TYPE_CHECKING:
    from simorgh.apps.tenants.models import Tenant


class SubscriptionError(Exception):
    """Raised for business rule violations in the subscription domain."""


# ---------------------------------------------------------------------------
# Subscription lifecycle
# ---------------------------------------------------------------------------


@transaction.atomic
def start_trial(tenant: Tenant, plan_code: str) -> TenantSubscription:
    """Create a new trial subscription for a tenant.

    Raises SubscriptionError if a subscription already exists.
    """
    from simorgh.apps.events.bus import dispatch

    if TenantSubscription.objects.filter(tenant=tenant).exists():
        raise SubscriptionError(
            _("Tenant already has a subscription. Use change_plan() to switch plans.")
        )

    plan = _require_plan(plan_code)
    now = timezone.now()
    sub = TenantSubscription.objects.create(
        tenant=tenant,
        organization_node=_require_root_node(tenant),
        plan=plan,
        status=SubscriptionStatus.TRIAL,
        trial_ends_at=now + timezone.timedelta(days=plan.trial_days),
        current_period_start=now,
        current_period_end=now + timezone.timedelta(days=plan.trial_days),
    )
    dispatch(
        "subscription.trial_started",
        payload={
            "tenant_id": tenant.pk,
            "plan_code": plan_code,
            "trial_ends_at": sub.trial_ends_at.isoformat(),
        },
    )
    return sub


@transaction.atomic
def create_subscription(
    tenant: Tenant,
    plan_code: str,
    *,
    billing_cycle: str = "monthly",
    actor=None,
) -> TenantSubscription:
    """Create or replace a subscription for a tenant as active.

    If a trial subscription exists it is upgraded in place;
    otherwise a new subscription is created.
    """
    from simorgh.apps.events.bus import dispatch

    plan = _require_plan(plan_code)
    now = timezone.now()

    if billing_cycle == "yearly":
        period_end = now + timezone.timedelta(days=365)
    else:
        period_end = now + timezone.timedelta(days=30)

    sub, created = TenantSubscription.objects.update_or_create(
        tenant=tenant,
        defaults={
            "organization_node": _require_root_node(tenant),
            "plan": plan,
            "status": SubscriptionStatus.ACTIVE,
            "current_period_start": now,
            "current_period_end": period_end,
        },
    )
    dispatch(
        "subscription.activated",
        payload={
            "tenant_id": tenant.pk,
            "plan_code": plan_code,
            "actor_id": actor.pk if actor else None,
        },
    )
    return sub


@transaction.atomic
def activate_subscription(sub_id, *, actor=None) -> TenantSubscription:
    """Activate a suspended or trial subscription."""
    from simorgh.apps.events.bus import dispatch

    sub = _require_subscription(sub_id)
    sub.status = SubscriptionStatus.ACTIVE
    sub.save(update_fields=["status", "updated_at"])
    dispatch(
        "subscription.activated",
        payload={
            "tenant_id": sub.tenant_id,
            "plan_code": sub.plan.code,
            "actor_id": actor.pk if actor else None,
        },
    )
    return sub


@transaction.atomic
def suspend_subscription(sub_id, *, reason: str = "", actor=None) -> TenantSubscription:
    """Suspend an active subscription."""
    from simorgh.apps.events.bus import dispatch

    sub = _require_subscription(sub_id)
    sub.status = SubscriptionStatus.SUSPENDED
    sub.save(update_fields=["status", "updated_at"])
    dispatch(
        "subscription.suspended",
        payload={
            "tenant_id": sub.tenant_id,
            "reason": reason,
            "actor_id": actor.pk if actor else None,
        },
    )
    return sub


@transaction.atomic
def cancel_subscription(sub_id, *, reason: str = "", actor=None) -> TenantSubscription:
    """Cancel a subscription."""
    from simorgh.apps.events.bus import dispatch

    sub = _require_subscription(sub_id)
    sub.status = SubscriptionStatus.CANCELLED
    sub.cancelled_at = timezone.now()
    sub.cancellation_reason = reason
    sub.save(update_fields=["status", "cancelled_at", "cancellation_reason", "updated_at"])
    dispatch(
        "subscription.cancelled",
        payload={
            "tenant_id": sub.tenant_id,
            "reason": reason,
            "actor_id": actor.pk if actor else None,
        },
    )
    return sub


@transaction.atomic
def change_plan(sub_id, new_plan_code: str, *, actor=None) -> TenantSubscription:
    """Upgrade or downgrade a subscription to a different plan."""
    from simorgh.apps.events.bus import dispatch

    sub = _require_subscription(sub_id)
    old_plan_code = sub.plan.code
    new_plan = _require_plan(new_plan_code)
    sub.plan = new_plan
    sub.status = SubscriptionStatus.ACTIVE
    sub.save(update_fields=["plan", "status", "updated_at"])
    dispatch(
        "subscription.plan_changed",
        payload={
            "tenant_id": sub.tenant_id,
            "old_plan_code": old_plan_code,
            "new_plan_code": new_plan_code,
            "actor_id": actor.pk if actor else None,
        },
    )
    return sub


# ---------------------------------------------------------------------------
# Add-on management
# ---------------------------------------------------------------------------


@transaction.atomic
def purchase_addon(
    tenant: Tenant,
    feature_key: str,
    *,
    actor=None,
    expires_at=None,
    price_paid=0,
) -> TenantAddonFeature:
    """Purchase or renew an add-on feature for a tenant."""
    from simorgh.apps.events.bus import dispatch

    addon, _ = TenantAddonFeature.objects.update_or_create(
        tenant=tenant,
        feature_key=feature_key,
        defaults={
            "organization_node": _require_root_node(tenant),
            "purchased_at": timezone.now(),
            "expires_at": expires_at,
            "price_paid": price_paid,
        },
    )
    dispatch(
        "subscription.addon_purchased",
        payload={
            "tenant_id": tenant.pk,
            "feature_key": feature_key,
            "actor_id": actor.pk if actor else None,
        },
    )
    return addon


@transaction.atomic
def revoke_addon(tenant: Tenant, feature_key: str, *, actor=None) -> None:
    """Remove an add-on feature from a tenant."""
    TenantAddonFeature.objects.filter(
        tenant=tenant, feature_key=feature_key
    ).delete()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _require_root_node(tenant):
    """Return the root OrganizationNode for a tenant, or raise SubscriptionError."""
    from simorgh.apps.organizations.selectors import get_root_nodes

    root = get_root_nodes(tenant.pk).first()
    if root is None:
        raise SubscriptionError(
            f"Tenant '{tenant.slug}' has no root organization node. "
            "Please complete tenant setup before creating a subscription."
        )
    return root


def _require_plan(plan_code: str) -> PlatformPlan:
    try:
        return PlatformPlan.objects.get(code=plan_code, is_active=True)
    except PlatformPlan.DoesNotExist:
        raise SubscriptionError(f"Plan '{plan_code}' does not exist or is inactive.")


def _require_subscription(sub_id) -> TenantSubscription:
    try:
        return TenantSubscription.objects.select_related("plan").get(pk=sub_id)
    except TenantSubscription.DoesNotExist:
        raise SubscriptionError(f"Subscription '{sub_id}' not found.")
