# @File: backend/routers/bizmail_client.py
# @Desc: Client-facing Bizmail360 endpoints. Every write enforces that
#        the calling user is the owning (domain-admin) user of the
#        subscription — any mailbox / alias / forward change is strictly
#        scoped to the caller's own domain.
from __future__ import annotations

import logging
from typing import List

from core.database import get_db
from dependencies.auth import get_current_user
from fastapi import APIRouter, Depends, HTTPException, status
from models.bizmail import BizmailMailbox
from schemas.auth import UserResponse
from schemas.bizmail import (
    BizmailActivationResult,
    BizmailAliasCreate,
    BizmailAliasOut,
    BizmailForwardingOut,
    BizmailForwardingUpdate,
    BizmailMailboxCreate,
    BizmailMailboxOut,
    BizmailMailboxPasswordReset,
    BizmailMailboxUpdate,
    BizmailPlanOut,
    BizmailSubscriptionActionResult,
    BizmailSubscriptionCreate,
    BizmailSubscriptionDetail,
    BizmailSubscriptionOut,
    BizmailUsageOut,
)
from services.bizmail import domains as smartermail
from services.bizmail.client import BizmailApiError
from services.bizmail.plans import get_plan
from services.bizmail.subscriptions import (
    activate_subscription,
    cancel_subscription,
    create_subscription,
    get_subscription_for_user,
    list_user_subscriptions,
    plan_for_subscription,
    record_audit,
    refresh_usage,
)
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/v1/client/bizmail", tags=["bizmail-client"])


# ---------- Helpers ----------
async def _build_detail(db: AsyncSession, sub) -> BizmailSubscriptionDetail:
    """Attach denormalised plan info to a subscription for the detail view."""
    plan = await plan_for_subscription(db, sub)
    detail = BizmailSubscriptionDetail.model_validate(sub)
    if plan is not None:
        detail.plan = BizmailPlanOut.model_validate(plan)
        detail.mailbox_quota = plan.mailbox_quota
        detail.aliases_included = plan.aliases_included
        detail.storage_mb_per_mailbox = plan.storage_mb_per_mailbox
    return detail


def _require_active(sub) -> None:
    """Reject mutations when the subscription is not in the `active` state."""
    if sub.status != "active":
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Subscription is {sub.status}; activate it first.",
        )


async def _load_owned_sub(
    db: AsyncSession, sub_id: int, current_user: UserResponse
):
    """Load a subscription or raise 404 if not owned by the caller."""
    sub = await get_subscription_for_user(db, sub_id, current_user.id)
    if sub is None:
        raise HTTPException(status_code=404, detail="Subscription not found.")
    return sub


def _full_address(local_part: str, domain: str) -> str:
    """Combine a local part and a domain into a normalised email address."""
    local = local_part.strip().lower()
    if "@" in local:
        raise HTTPException(status_code=400, detail="Username must not contain '@'.")
    if not local:
        raise HTTPException(status_code=400, detail="Username is required.")
    return f"{local}@{domain}"


# ---------- Subscriptions ----------
@router.get("/subscriptions", response_model=List[BizmailSubscriptionDetail])
async def list_my_subscriptions(
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    """List every Bizmail360 subscription owned by the caller."""
    subs = await list_user_subscriptions(db, current_user.id)
    return [await _build_detail(db, s) for s in subs]


@router.post(
    "/subscriptions",
    response_model=BizmailSubscriptionDetail,
    status_code=status.HTTP_201_CREATED,
)
async def order_subscription(
    payload: BizmailSubscriptionCreate,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    """Place a new Bizmail360 order (creates a pending subscription)."""
    plan = await get_plan(db, payload.plan_id)
    if plan is None or not plan.is_active:
        raise HTTPException(status_code=404, detail="Plan not found or inactive.")
    try:
        sub = await create_subscription(
            db,
            user_id=current_user.id,
            user_email=current_user.email,
            plan=plan,
            domain_name=payload.domain_name,
            billing_cycle=payload.billing_cycle,
        )
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    return await _build_detail(db, sub)


@router.get("/subscriptions/{sub_id}", response_model=BizmailSubscriptionDetail)
async def get_my_subscription(
    sub_id: int,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    sub = await _load_owned_sub(db, sub_id, current_user)
    return await _build_detail(db, sub)


@router.post(
    "/subscriptions/{sub_id}/activate",
    response_model=BizmailActivationResult,
)
async def activate_my_subscription(
    sub_id: int,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    """Activate a pending subscription — provisions the domain upstream.

    Note: the unified checkout/payment flow will call this automatically
    once a payment is verified. Exposing it here lets us trial the feature
    end-to-end before the checkout module lands, and it is idempotent.
    """
    sub = await _load_owned_sub(db, sub_id, current_user)
    if sub.status == "cancelled":
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Subscription is cancelled and cannot be reactivated.",
        )
    plan = await plan_for_subscription(db, sub)
    if plan is None:
        raise HTTPException(status_code=404, detail="Plan not found for this subscription.")
    try:
        sub, password = await activate_subscription(
            db, sub, plan=plan, actor_email=current_user.email
        )
    except BizmailApiError as exc:
        raise HTTPException(status_code=502, detail=exc.message) from exc
    return BizmailActivationResult(
        subscription=await _build_detail(db, sub),
        primary_mailbox=sub.primary_mailbox,
        primary_password=password,
    )


@router.post(
    "/subscriptions/{sub_id}/cancel",
    response_model=BizmailSubscriptionActionResult,
)
async def cancel_my_subscription(
    sub_id: int,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    """Cancel the caller's subscription (and wipe the upstream domain)."""
    sub = await _load_owned_sub(db, sub_id, current_user)
    if sub.status == "cancelled":
        raise HTTPException(status_code=409, detail="Already cancelled.")
    sub = await cancel_subscription(
        db, sub, actor_email=current_user.email, delete_remote=True
    )
    return BizmailSubscriptionActionResult(
        subscription=await _build_detail(db, sub),
        message="Subscription cancelled.",
    )


# ---------- Mailboxes ----------
@router.get(
    "/subscriptions/{sub_id}/mailboxes", response_model=List[BizmailMailboxOut]
)
async def list_mailboxes(
    sub_id: int,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    sub = await _load_owned_sub(db, sub_id, current_user)
    _require_active(sub)
    try:
        users = await smartermail.list_users(sub.domain_name)
    except BizmailApiError as exc:
        raise HTTPException(status_code=502, detail=exc.message) from exc
    return [
        BizmailMailboxOut(
            username=u.get("username") or u.get("emailAddress") or "",
            display_name=u.get("displayName"),
            is_domain_admin=bool(u.get("isDomainAdmin", False)),
            disabled=not bool(u.get("enabled", True)),
            storage_mb=u.get("maxMailboxSize") if isinstance(u.get("maxMailboxSize"), int) else None,
        )
        for u in users
        if u.get("username") or u.get("emailAddress")
    ]


@router.post(
    "/subscriptions/{sub_id}/mailboxes",
    response_model=BizmailMailboxOut,
    status_code=status.HTTP_201_CREATED,
)
async def create_mailbox(
    sub_id: int,
    payload: BizmailMailboxCreate,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    sub = await _load_owned_sub(db, sub_id, current_user)
    _require_active(sub)
    plan = await plan_for_subscription(db, sub)
    if plan is None:
        raise HTTPException(status_code=404, detail="Plan not found for this subscription.")
    if sub.mailboxes_used >= plan.mailbox_quota:
        raise HTTPException(
            status_code=409,
            detail=f"Mailbox quota reached ({plan.mailbox_quota}).",
        )
    full = _full_address(payload.username, sub.domain_name)
    try:
        await smartermail.create_user(
            domain_name=sub.domain_name,
            username=full,
            password=payload.password,
            display_name=payload.display_name,
            is_domain_admin=payload.is_domain_admin,
            mailbox_size_mb=plan.storage_mb_per_mailbox,
        )
    except BizmailApiError as exc:
        await record_audit(
            db,
            subscription_id=sub.id,
            actor_user_id=sub.user_id,
            actor_email=current_user.email,
            action="mailbox.create",
            target=full,
            status="error",
            error_message=exc.message,
        )
        raise HTTPException(status_code=502, detail=exc.message) from exc
    db.add(
        BizmailMailbox(
            subscription_id=sub.id,
            user_id=sub.user_id,
            username=full,
            display_name=payload.display_name,
            is_domain_admin=payload.is_domain_admin,
        )
    )
    sub.mailboxes_used = (sub.mailboxes_used or 0) + 1
    await db.commit()
    await record_audit(
        db,
        subscription_id=sub.id,
        actor_user_id=sub.user_id,
        actor_email=current_user.email,
        action="mailbox.create",
        target=full,
    )
    return BizmailMailboxOut(
        username=full,
        display_name=payload.display_name,
        is_domain_admin=payload.is_domain_admin,
    )


@router.put(
    "/subscriptions/{sub_id}/mailboxes/{username}",
    response_model=BizmailMailboxOut,
)
async def update_mailbox(
    sub_id: int,
    username: str,
    payload: BizmailMailboxUpdate,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    sub = await _load_owned_sub(db, sub_id, current_user)
    _require_active(sub)
    if not username.endswith("@" + sub.domain_name):
        raise HTTPException(status_code=400, detail="Mailbox does not belong to this domain.")
    try:
        await smartermail.update_user(
            username=username,
            display_name=payload.display_name,
            disabled=payload.disabled,
        )
    except BizmailApiError as exc:
        raise HTTPException(status_code=502, detail=exc.message) from exc
    # Sync local cache
    mbox = (
        await db.execute(
            select(BizmailMailbox).where(
                BizmailMailbox.subscription_id == sub.id,
                BizmailMailbox.username == username,
            )
        )
    ).scalar_one_or_none()
    if mbox is not None:
        if payload.display_name is not None:
            mbox.display_name = payload.display_name
        if payload.disabled is not None:
            mbox.disabled = payload.disabled
        await db.commit()
    await record_audit(
        db,
        subscription_id=sub.id,
        actor_user_id=sub.user_id,
        actor_email=current_user.email,
        action="mailbox.update",
        target=username,
        payload={"display_name": payload.display_name, "disabled": payload.disabled},
    )
    return BizmailMailboxOut(
        username=username,
        display_name=payload.display_name,
        disabled=bool(payload.disabled) if payload.disabled is not None else False,
    )


@router.post(
    "/subscriptions/{sub_id}/mailboxes/{username}/reset-password",
    response_model=BizmailMailboxOut,
)
async def reset_mailbox_password(
    sub_id: int,
    username: str,
    payload: BizmailMailboxPasswordReset,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    sub = await _load_owned_sub(db, sub_id, current_user)
    _require_active(sub)
    if not username.endswith("@" + sub.domain_name):
        raise HTTPException(status_code=400, detail="Mailbox does not belong to this domain.")
    try:
        await smartermail.reset_user_password(username, payload.new_password)
    except BizmailApiError as exc:
        raise HTTPException(status_code=502, detail=exc.message) from exc
    await record_audit(
        db,
        subscription_id=sub.id,
        actor_user_id=sub.user_id,
        actor_email=current_user.email,
        action="mailbox.reset_password",
        target=username,
    )
    return BizmailMailboxOut(username=username)


@router.delete(
    "/subscriptions/{sub_id}/mailboxes/{username}",
    response_model=BizmailSubscriptionActionResult,
)
async def delete_mailbox(
    sub_id: int,
    username: str,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    sub = await _load_owned_sub(db, sub_id, current_user)
    _require_active(sub)
    if not username.endswith("@" + sub.domain_name):
        raise HTTPException(status_code=400, detail="Mailbox does not belong to this domain.")
    if sub.primary_mailbox and username == sub.primary_mailbox:
        raise HTTPException(status_code=409, detail="Cannot delete the domain administrator mailbox.")
    try:
        await smartermail.delete_user(username)
    except BizmailApiError as exc:
        raise HTTPException(status_code=502, detail=exc.message) from exc
    # Remove local cache
    mbox = (
        await db.execute(
            select(BizmailMailbox).where(
                BizmailMailbox.subscription_id == sub.id,
                BizmailMailbox.username == username,
            )
        )
    ).scalar_one_or_none()
    if mbox is not None:
        await db.delete(mbox)
    sub.mailboxes_used = max(0, (sub.mailboxes_used or 1) - 1)
    await db.commit()
    await record_audit(
        db,
        subscription_id=sub.id,
        actor_user_id=sub.user_id,
        actor_email=current_user.email,
        action="mailbox.delete",
        target=username,
    )
    return BizmailSubscriptionActionResult(
        subscription=await _build_detail(db, sub),
        message="Mailbox deleted.",
    )


# ---------- Aliases ----------
@router.get(
    "/subscriptions/{sub_id}/aliases", response_model=List[BizmailAliasOut]
)
async def list_aliases(
    sub_id: int,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    sub = await _load_owned_sub(db, sub_id, current_user)
    _require_active(sub)
    try:
        rows = await smartermail.list_aliases(sub.domain_name)
    except BizmailApiError as exc:
        raise HTTPException(status_code=502, detail=exc.message) from exc
    out: List[BizmailAliasOut] = []
    for a in rows:
        name = a.get("name") or a.get("alias") or ""
        addrs = a.get("addresses") or a.get("targets") or []
        if isinstance(addrs, str):
            addrs = [addrs]
        if name:
            out.append(BizmailAliasOut(name=name, addresses=list(addrs)))
    return out


@router.post(
    "/subscriptions/{sub_id}/aliases",
    response_model=BizmailAliasOut,
    status_code=status.HTTP_201_CREATED,
)
async def create_alias(
    sub_id: int,
    payload: BizmailAliasCreate,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    sub = await _load_owned_sub(db, sub_id, current_user)
    _require_active(sub)
    plan = await plan_for_subscription(db, sub)
    if plan is None:
        raise HTTPException(status_code=404, detail="Plan not found.")
    if sub.aliases_used >= plan.aliases_included:
        raise HTTPException(status_code=409, detail=f"Alias quota reached ({plan.aliases_included}).")
    full = _full_address(payload.alias_name, sub.domain_name)
    try:
        await smartermail.create_alias(
            domain_name=sub.domain_name,
            alias_name=full,
            targets=payload.targets,
        )
    except BizmailApiError as exc:
        raise HTTPException(status_code=502, detail=exc.message) from exc
    sub.aliases_used = (sub.aliases_used or 0) + 1
    await db.commit()
    await record_audit(
        db,
        subscription_id=sub.id,
        actor_user_id=sub.user_id,
        actor_email=current_user.email,
        action="alias.create",
        target=full,
        payload={"targets": payload.targets},
    )
    return BizmailAliasOut(name=full, addresses=list(payload.targets))


@router.delete(
    "/subscriptions/{sub_id}/aliases/{alias_name}",
    response_model=BizmailSubscriptionActionResult,
)
async def delete_alias(
    sub_id: int,
    alias_name: str,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    sub = await _load_owned_sub(db, sub_id, current_user)
    _require_active(sub)
    if not alias_name.endswith("@" + sub.domain_name):
        raise HTTPException(status_code=400, detail="Alias does not belong to this domain.")
    try:
        await smartermail.delete_alias(sub.domain_name, alias_name)
    except BizmailApiError as exc:
        raise HTTPException(status_code=502, detail=exc.message) from exc
    sub.aliases_used = max(0, (sub.aliases_used or 1) - 1)
    await db.commit()
    await record_audit(
        db,
        subscription_id=sub.id,
        actor_user_id=sub.user_id,
        actor_email=current_user.email,
        action="alias.delete",
        target=alias_name,
    )
    return BizmailSubscriptionActionResult(
        subscription=await _build_detail(db, sub),
        message="Alias deleted.",
    )


# ---------- Forwards ----------
@router.get(
    "/subscriptions/{sub_id}/mailboxes/{username}/forwarding",
    response_model=BizmailForwardingOut,
)
async def get_forwarding(
    sub_id: int,
    username: str,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    sub = await _load_owned_sub(db, sub_id, current_user)
    _require_active(sub)
    if not username.endswith("@" + sub.domain_name):
        raise HTTPException(status_code=400, detail="Mailbox does not belong to this domain.")
    data = await smartermail.get_user_forwarding(username)
    return BizmailForwardingOut(
        username=username,
        forward_to=data.get("forwardingAddress") or "",
        enabled=bool(data.get("enabled") or data.get("forwardingAddress")),
    )


@router.put(
    "/subscriptions/{sub_id}/mailboxes/{username}/forwarding",
    response_model=BizmailForwardingOut,
)
async def set_forwarding(
    sub_id: int,
    username: str,
    payload: BizmailForwardingUpdate,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    sub = await _load_owned_sub(db, sub_id, current_user)
    _require_active(sub)
    if not username.endswith("@" + sub.domain_name):
        raise HTTPException(status_code=400, detail="Mailbox does not belong to this domain.")
    try:
        await smartermail.set_user_forwarding(username, payload.forward_to)
    except BizmailApiError as exc:
        raise HTTPException(status_code=502, detail=exc.message) from exc
    await record_audit(
        db,
        subscription_id=sub.id,
        actor_user_id=sub.user_id,
        actor_email=current_user.email,
        action="mailbox.forwarding",
        target=username,
        payload={"forward_to": payload.forward_to},
    )
    return BizmailForwardingOut(
        username=username,
        forward_to=payload.forward_to,
        enabled=bool(payload.forward_to),
    )


# ---------- Usage ----------
@router.get("/subscriptions/{sub_id}/usage", response_model=BizmailUsageOut)
async def get_usage(
    sub_id: int,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    sub = await _load_owned_sub(db, sub_id, current_user)
    plan = await plan_for_subscription(db, sub)
    if plan is None:
        raise HTTPException(status_code=404, detail="Plan not found.")
    try:
        await refresh_usage(db, sub)
    except BizmailApiError as exc:
        return BizmailUsageOut(
            mailboxes_used=sub.mailboxes_used or 0,
            mailbox_quota=plan.mailbox_quota,
            aliases_used=sub.aliases_used or 0,
            aliases_included=plan.aliases_included,
            storage_used_mb=0,
            storage_quota_mb=plan.storage_mb_per_mailbox * plan.mailbox_quota,
            degraded=True,
            message=exc.message,
        )
    return BizmailUsageOut(
        mailboxes_used=sub.mailboxes_used or 0,
        mailbox_quota=plan.mailbox_quota,
        aliases_used=sub.aliases_used or 0,
        aliases_included=plan.aliases_included,
        storage_used_mb=0,
        storage_quota_mb=plan.storage_mb_per_mailbox * plan.mailbox_quota,
        degraded=False,
    )


@router.get("/subscriptions/{sub_id}/plan", response_model=BizmailPlanOut)
async def get_plan_for_sub(
    sub_id: int,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    sub = await _load_owned_sub(db, sub_id, current_user)
    plan = await plan_for_subscription(db, sub)
    if plan is None:
        raise HTTPException(status_code=404, detail="Plan not found.")
    return BizmailPlanOut.model_validate(plan)


# Expose list type alias so the frontend can share the subscription-list shape.
# (Kept for schema clarity; FastAPI does not require it.)
_ = BizmailSubscriptionOut