# @File: backend/services/bizmail/subscriptions.py
# @Desc: Lifecycle helpers for BizmailSubscription — create pending
#        orders, activate (provision on SmarterMail), suspend, cancel,
#        and resync cached usage counters.
#
# Provisioning strategy:
#   * create_subscription() only inserts a DB row with status="pending".
#   * activate_subscription() is the single place that calls SmarterMail
#     to actually create the domain + postmaster mailbox. It is idempotent:
#     if the domain already exists upstream we skip creation but still
#     mark the row as active so a manual re-activate never double-creates.
#   * suspend / cancel mirror the state change to SmarterMail by disabling
#     the domain. cancel() additionally wipes the upstream domain unless
#     the caller passes ``delete_remote=False``.
from __future__ import annotations

import json
import logging
import secrets
import string
from datetime import datetime, timedelta, timezone
from typing import List, Optional, Tuple

from models.bizmail import (
    BizmailAuditLog,
    BizmailMailbox,
    BizmailPlan,
    BizmailSubscription,
)
from services.bizmail import domains as smartermail
from services.bizmail.client import BizmailApiError
from services.bizmail.plans import get_plan
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession

logger = logging.getLogger(__name__)


# ---------- Audit helper ----------
async def record_audit(
    db: AsyncSession,
    *,
    subscription_id: Optional[int],
    actor_user_id: Optional[str],
    actor_email: Optional[str],
    action: str,
    target: Optional[str] = None,
    status: str = "ok",
    payload: Optional[dict] = None,
    error_message: Optional[str] = None,
) -> None:
    """Write a single row to the Bizmail audit log. Never raises."""
    try:
        row = BizmailAuditLog(
            subscription_id=subscription_id,
            actor_user_id=actor_user_id,
            actor_email=actor_email,
            action=action,
            target=target,
            status=status,
            payload=json.dumps(payload)[:4000] if payload else None,
            error_message=error_message,
        )
        db.add(row)
        await db.commit()
    except Exception:  # noqa: BLE001
        # Audit failures must never crash the caller.
        logger.warning("Failed to record Bizmail audit row for %s", action, exc_info=True)
        await db.rollback()


# ---------- Subscription CRUD ----------
async def list_user_subscriptions(
    db: AsyncSession, user_id: str
) -> List[BizmailSubscription]:
    stmt = (
        select(BizmailSubscription)
        .where(BizmailSubscription.user_id == user_id)
        .order_by(BizmailSubscription.created_at.desc())
    )
    return list((await db.execute(stmt)).scalars().all())


async def list_all_subscriptions(
    db: AsyncSession, *, status: Optional[str] = None
) -> List[BizmailSubscription]:
    stmt = select(BizmailSubscription)
    if status:
        stmt = stmt.where(BizmailSubscription.status == status)
    stmt = stmt.order_by(BizmailSubscription.created_at.desc())
    return list((await db.execute(stmt)).scalars().all())


async def get_subscription(
    db: AsyncSession, sub_id: int
) -> Optional[BizmailSubscription]:
    return (
        await db.execute(
            select(BizmailSubscription).where(BizmailSubscription.id == sub_id)
        )
    ).scalar_one_or_none()


async def get_subscription_for_user(
    db: AsyncSession, sub_id: int, user_id: str
) -> Optional[BizmailSubscription]:
    """Return a subscription only if it belongs to the given user.

    Used by the client-facing router to enforce ownership on every write.
    """
    return (
        await db.execute(
            select(BizmailSubscription).where(
                and_(
                    BizmailSubscription.id == sub_id,
                    BizmailSubscription.user_id == user_id,
                )
            )
        )
    ).scalar_one_or_none()


def _normalise_domain(domain: str) -> str:
    """Lower-case + strip whitespace; used both for keying and display."""
    return domain.strip().lower()


async def _domain_is_unique(db: AsyncSession, domain: str) -> bool:
    stmt = select(BizmailSubscription.id).where(
        BizmailSubscription.domain_name == domain,
        BizmailSubscription.status.in_(("pending", "active", "suspended")),
    )
    return (await db.execute(stmt)).scalar_one_or_none() is None


async def create_subscription(
    db: AsyncSession,
    *,
    user_id: str,
    user_email: Optional[str],
    plan: BizmailPlan,
    domain_name: str,
    billing_cycle: str = "monthly",
) -> BizmailSubscription:
    """Create a pending subscription. Does NOT touch SmarterMail yet."""
    domain_name = _normalise_domain(domain_name)
    if "." not in domain_name or " " in domain_name:
        raise ValueError("Domain name looks invalid.")
    if not await _domain_is_unique(db, domain_name):
        raise ValueError("This domain is already registered in Bizmail360.")

    sub = BizmailSubscription(
        user_id=user_id,
        user_email=user_email,
        plan_id=plan.id,
        domain_name=domain_name,
        status="pending",
        billing_cycle=billing_cycle if billing_cycle in ("monthly", "yearly") else "monthly",
    )
    db.add(sub)
    await db.commit()
    await db.refresh(sub)
    await record_audit(
        db,
        subscription_id=sub.id,
        actor_user_id=user_id,
        actor_email=user_email,
        action="subscription.create",
        target=domain_name,
        payload={"plan_id": plan.id, "billing_cycle": sub.billing_cycle},
    )
    return sub


def _generate_password(length: int = 16) -> str:
    """Generate a strong domain-admin password.

    Uses letters + digits + a small ASCII-safe symbol set SmarterMail
    accepts without URL-encoding headaches.
    """
    alphabet = string.ascii_letters + string.digits + "!@#$%&*-_"
    # Guarantee at least one of each class for password policies.
    picks = [
        secrets.choice(string.ascii_uppercase),
        secrets.choice(string.ascii_lowercase),
        secrets.choice(string.digits),
        secrets.choice("!@#$%&*-_"),
    ]
    picks += [secrets.choice(alphabet) for _ in range(max(0, length - len(picks)))]
    secrets.SystemRandom().shuffle(picks)
    return "".join(picks)


async def activate_subscription(
    db: AsyncSession,
    sub: BizmailSubscription,
    *,
    plan: BizmailPlan,
    actor_email: Optional[str],
) -> Tuple[BizmailSubscription, Optional[str]]:
    """Provision the domain on SmarterMail and mark the sub as active.

    Returns (subscription, primary_admin_password). The password is only
    returned on the initial provisioning call — on re-activation it is
    ``None`` because SmarterMail cannot reveal the existing password.
    """
    primary_admin = f"admin@{sub.domain_name}"
    primary_password: Optional[str] = None
    try:
        # Always (re)load the plan so quotas are up-to-date.
        if sub.provisioned_at is None:
            primary_password = _generate_password()
            await smartermail.create_domain(
                domain_name=sub.domain_name,
                storage_mb=plan.storage_mb_per_mailbox,
                mailbox_quota=plan.mailbox_quota,
                aliases_allowed=plan.aliases_included,
                primary_admin_username=primary_admin,
                primary_admin_password=primary_password,
                primary_admin_display="Domain Administrator",
            )
            sub.provisioned_at = datetime.now(timezone.utc)
            sub.primary_mailbox = primary_admin
        else:
            # Already provisioned upstream — push the latest plan quotas.
            try:
                await smartermail.update_domain(
                    sub.domain_name,
                    {
                        "maxDomainSizeInMB": plan.storage_mb_per_mailbox * plan.mailbox_quota,
                        "maxMailboxSize": plan.storage_mb_per_mailbox,
                        "maxUsers": plan.mailbox_quota,
                        "maxAliases": plan.aliases_included,
                        "enabled": True,
                    },
                )
            except BizmailApiError as exc:
                logger.info(
                    "Non-fatal: failed to push quota update on re-activate for %s: %s",
                    sub.domain_name,
                    exc.message,
                )
        sub.status = "active"
        sub.activated_at = datetime.now(timezone.utc)
        sub.suspended_at = None
        sub.cancelled_at = None
        # Set a default 30- or 365-day period so the dashboard has a
        # reasonable renewal date before the unified checkout module lands.
        period_days = 365 if sub.billing_cycle == "yearly" else 30
        sub.current_period_end = datetime.now(timezone.utc) + timedelta(days=period_days)
        if primary_password:
            # Mirror the postmaster into our local mailbox cache.
            mbox = BizmailMailbox(
                subscription_id=sub.id,
                user_id=sub.user_id,
                username=primary_admin,
                display_name="Domain Administrator",
                is_domain_admin=True,
                last_synced_at=datetime.now(timezone.utc),
            )
            db.add(mbox)
        await db.commit()
        await db.refresh(sub)
        await record_audit(
            db,
            subscription_id=sub.id,
            actor_user_id=sub.user_id,
            actor_email=actor_email,
            action="subscription.activate",
            target=sub.domain_name,
        )
        return sub, primary_password
    except BizmailApiError as exc:
        await db.rollback()
        await record_audit(
            db,
            subscription_id=sub.id,
            actor_user_id=sub.user_id,
            actor_email=actor_email,
            action="subscription.activate",
            target=sub.domain_name,
            status="error",
            error_message=exc.message,
        )
        raise


async def suspend_subscription(
    db: AsyncSession,
    sub: BizmailSubscription,
    *,
    actor_email: Optional[str],
) -> BizmailSubscription:
    """Mark the subscription suspended and disable the upstream domain."""
    try:
        if sub.provisioned_at:
            try:
                await smartermail.update_domain(sub.domain_name, {"enabled": False})
            except BizmailApiError as exc:
                logger.info(
                    "Upstream disable failed for %s, marking local suspended anyway: %s",
                    sub.domain_name,
                    exc.message,
                )
        sub.status = "suspended"
        sub.suspended_at = datetime.now(timezone.utc)
        await db.commit()
        await db.refresh(sub)
        await record_audit(
            db,
            subscription_id=sub.id,
            actor_user_id=sub.user_id,
            actor_email=actor_email,
            action="subscription.suspend",
            target=sub.domain_name,
        )
        return sub
    finally:
        pass


async def resume_subscription(
    db: AsyncSession,
    sub: BizmailSubscription,
    *,
    actor_email: Optional[str],
) -> BizmailSubscription:
    """Re-enable a suspended subscription."""
    if sub.provisioned_at:
        try:
            await smartermail.update_domain(sub.domain_name, {"enabled": True})
        except BizmailApiError as exc:
            logger.info("Upstream enable failed for %s: %s", sub.domain_name, exc.message)
    sub.status = "active"
    sub.suspended_at = None
    await db.commit()
    await db.refresh(sub)
    await record_audit(
        db,
        subscription_id=sub.id,
        actor_user_id=sub.user_id,
        actor_email=actor_email,
        action="subscription.resume",
        target=sub.domain_name,
    )
    return sub


async def cancel_subscription(
    db: AsyncSession,
    sub: BizmailSubscription,
    *,
    actor_email: Optional[str],
    delete_remote: bool = True,
) -> BizmailSubscription:
    """Cancel a subscription and optionally wipe the upstream domain."""
    if delete_remote and sub.provisioned_at:
        try:
            await smartermail.delete_domain(sub.domain_name)
        except BizmailApiError as exc:
            logger.info(
                "Upstream delete failed for %s (cancel proceeds): %s",
                sub.domain_name,
                exc.message,
            )
    sub.status = "cancelled"
    sub.cancelled_at = datetime.now(timezone.utc)
    await db.commit()
    await db.refresh(sub)
    await record_audit(
        db,
        subscription_id=sub.id,
        actor_user_id=sub.user_id,
        actor_email=actor_email,
        action="subscription.cancel",
        target=sub.domain_name,
        payload={"delete_remote": delete_remote},
    )
    return sub


async def refresh_usage(db: AsyncSession, sub: BizmailSubscription) -> BizmailSubscription:
    """Pull latest usage counters from SmarterMail into the cached columns."""
    if not sub.provisioned_at:
        return sub
    usage = await smartermail.get_domain_usage(sub.domain_name)
    mbox = usage.get("mailboxes")
    aliases = usage.get("aliases")
    if isinstance(mbox, int):
        sub.mailboxes_used = mbox
    if isinstance(aliases, int):
        sub.aliases_used = aliases
    await db.commit()
    await db.refresh(sub)
    return sub


# ---------- Plan lookup (small wrapper for the routers) ----------
async def plan_for_subscription(
    db: AsyncSession, sub: BizmailSubscription
) -> Optional[BizmailPlan]:
    return await get_plan(db, sub.plan_id)