# @File: backend/routers/whmcs.py
# @Desc: WHMCS integration endpoints. All routes require an authenticated Atoms user;
#        we resolve the user's WHMCS client id by their email on every request.
import logging
from typing import Any, Dict, List, Optional

from fastapi import APIRouter, Depends, HTTPException

from dependencies.auth import get_current_user
from schemas.auth import UserResponse
from core.auth import create_access_token
from schemas.whmcs import (
    WhmcsCreateTicketRequest,
    WhmcsCreateTicketResponse,
    WhmcsDepartment,
    WhmcsDepartmentsResponse,
    WhmcsDomain,
    WhmcsEmail,
    WhmcsForgotPasswordRequest,
    WhmcsForgotPasswordResponse,
    WhmcsInvoice,
    WhmcsListResponse,
    WhmcsLoginRequest,
    WhmcsLoginResponse,
    WhmcsProfile,
    WhmcsRegisterRequest,
    WhmcsRegisterResponse,
    WhmcsReplyTicketRequest,
    WhmcsReplyTicketResponse,
    WhmcsResetPasswordRequest,
    WhmcsResetPasswordResponse,
    WhmcsService,
    WhmcsTicket,
    WhmcsTicketDetail,
    WhmcsTicketReply,
    WhmcsUpdateProfileRequest,
    WhmcsUpdateProfileResponse,
)
from services.password_reset import (
    build_reset_link,
    create_reset_token,
    send_reset_email_via_whmcs,
    sync_password_to_whmcs,
    verify_reset_token,
)
from services.whmcs import (
    call_whmcs,
    call_whmcs_safe,
    get_client_by_email,
    invalidate_client_cache,
)
from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/v1/whmcs", tags=["whmcs"])


def _safe_int(value: Any, default: int = 0) -> int:
    try:
        return int(value)
    except (TypeError, ValueError):
        return default


def _as_list(value: Any) -> List[Dict[str, Any]]:
    """Normalize WHMCS list-shaped fields.

    Depending on the action and number of results, WHMCS may return:
      - a list of dicts (many items)
      - a single dict (exactly one item)  <-- easy to drop by accident
      - the key missing entirely (zero items)
      - null / other types

    This helper always returns a clean ``List[Dict[str, Any]]`` so downstream
    parsing is uniform.
    """
    if value is None:
        return []
    if isinstance(value, list):
        return [item for item in value if isinstance(item, dict)]
    if isinstance(value, dict):
        return [value]
    return []


async def _require_whmcs_client(email: str) -> Dict[str, Any]:
    """Return the WHMCS client record for the given email or raise 404."""
    client = await get_client_by_email(email)
    if not client:
        raise HTTPException(
            status_code=404,
            detail="No WHMCS client linked to this account. Please complete registration.",
        )
    return client


def _admin_email_allowlist() -> set:
    """Return the lowercase set of emails that should receive admin privileges.

    Reads two sources, combined:
      1) ``ADMIN_EMAILS`` environment variable (comma / semicolon / space
         separated). This is the fast-path used by deployments — set it via
         the platform "Secrets" UI without redeploying code.
      2) Legacy fallback ``ADMIN_EMAIL`` (single value) for backward compat.

    Example:
        ADMIN_EMAILS="owner@example.com, ops@example.com"

    Why not a separate admins table?  Atoms Cloud + WHMCS already own the
    user database. Introducing a second auth system / admins table would
    create two truths. An env-driven allowlist lets us reuse the existing
    WHMCS-backed login flow and simply elevate selected accounts — the user
    signs in with the same email/password, and if their email is allowlisted
    the JWT is minted with ``role: "admin"`` so the frontend ``/admin`` gate
    in AdminDashboard.tsx accepts them.
    """
    import os

    raw = os.environ.get("ADMIN_EMAILS") or os.environ.get("ADMIN_EMAIL") or ""
    # Accept commas, semicolons, or whitespace as separators — humans are messy.
    normalised = raw.replace(";", ",").replace("\n", ",")
    parts = [p.strip().lower() for p in normalised.split(",") if p.strip()]
    return set(parts)


def _resolve_role_for_email(email: Optional[str]) -> str:
    """Decide the JWT role for a given login email (env allowlist only).

    Returns ``"admin"`` if the email is in the env allowlist, otherwise
    ``"user"``. Use :func:`_resolve_role_for_email_with_db` when a DB session
    is available to also honour DB-stored role assignments.
    """
    if not email:
        return "user"
    return "admin" if email.strip().lower() in _admin_email_allowlist() else "user"


async def _resolve_role_for_email_with_db(email: Optional[str]) -> str:
    """Like :func:`_resolve_role_for_email` but also consults the ``user_roles`` table.

    The DB-stored role map is populated by super-admins via the
    ``/admin/cms/roles`` page. This lets the primary admin invite additional
    admins without redeploying or editing env variables.
    """
    if not email:
        return "user"

    key = email.strip().lower()
    if key in _admin_email_allowlist():
        return "admin"

    # Lazy import keeps this module import-safe even if role_management
    # isn't registered yet (e.g. during tests or fresh boots).
    try:
        from core.database import db_manager  # type: ignore
        from routers.role_management import db_admin_role_map
    except Exception:
        return "user"

    session_maker = getattr(db_manager, "async_session_maker", None)
    if session_maker is None:
        return "user"

    try:
        async with session_maker() as session:
            db_role = (await db_admin_role_map(session)).get(key)
    except Exception:
        # Don't block login if the DB or table is unavailable — just fall
        # back to the env allowlist decision (already handled above).
        return "user"

    # Map our richer role names into the coarse JWT role the frontend gate
    # expects. 'super-admin' and 'admin' both unlock /admin; 'support' does
    # not (the support portal may check the richer role separately later).
    if db_role in ("super-admin", "admin"):
        return "admin"
    return "user"


async def _build_app_token(*, client_id: int, email: str, name: Optional[str]) -> str:
    """Mint an application JWT whose `sub` identifies a WHMCS client.

    Because WHMCS is the user database for this portal, we use a prefixed client
    id as the stable user identifier in our JWTs. The existing
    ``get_current_user`` dependency decodes this token without any changes.

    If the email is present in ``ADMIN_EMAILS`` *or* has been assigned an admin
    role in the ``user_roles`` table (see ``routers/role_management.py``) the
    token is minted with ``role: "admin"`` so the frontend ``/admin`` gate
    recognises the user as an administrator without requiring a second auth
    system.
    """
    subject = f"whmcs:{client_id}"
    role = await _resolve_role_for_email_with_db(email)
    claims: Dict[str, Any] = {
        "sub": subject,
        "email": email,
        "name": name or email,
        "role": role,
        "whmcs_client_id": client_id,
        "is_admin": role == "admin",
    }
    return create_access_token(claims)


@router.get("/health")
async def whmcs_public_health():
    """Public, unauthenticated liveness probe for the WHMCS router.

    This endpoint exists so the Login page can fire a "pre-warm" ping when it
    mounts, which wakes the backend container before the user clicks "Sign In".
    Previously the frontend pinged this path but no route was registered, so
    every ping returned 404 and never warmed anything — leaving the first real
    /api/v1/whmcs/login call to hit the Atoms Cloudflare 503 HTML shell during
    cold-start.

    Kept intentionally lightweight: does NOT contact WHMCS. Its only job is to
    force the serverless container to boot and complete a successful round-trip
    through Cloudflare + load balancer, so the next request is served warm.
    """
    return {"status": "ok", "service": "whmcs"}


@router.post("/register", response_model=WhmcsLoginResponse)
async def whmcs_register(payload: WhmcsRegisterRequest):
    """Create a WHMCS client via the AddClient API.

    This endpoint is public (no prior authentication required). On success we
    auto-issue an application JWT so the newly-registered user is signed in
    immediately. The frontend stores the token in `localStorage.token` and
    redirects to the dashboard.
    """
    # Prevent duplicate WHMCS clients for the same email.
    existing = await get_client_by_email(payload.email)
    if existing:
        raise HTTPException(
            status_code=409,
            detail="An account with this email already exists. Please sign in instead.",
        )

    result = await call_whmcs(
        "AddClient",
        {
            "firstname": payload.firstname,
            "lastname": payload.lastname,
            "email": payload.email,
            "password2": payload.password,
            "companyname": payload.companyname or "",
            "address1": payload.address1,
            "address2": payload.address2 or "",
            "city": payload.city,
            "state": payload.state,
            "postcode": payload.postcode,
            "country": payload.country.upper(),
            "phonenumber": payload.phonenumber,
            "noemail": False,
        },
    )

    client_id = _safe_int(result.get("clientid"))
    if client_id <= 0:
        raise HTTPException(status_code=502, detail="WHMCS did not return a client id.")

    full_name = f"{payload.firstname} {payload.lastname}".strip() or payload.email
    token = await _build_app_token(client_id=client_id, email=str(payload.email), name=full_name)
    return WhmcsLoginResponse(
        token=token,
        user_id=f"whmcs:{client_id}",
        email=payload.email,
        name=full_name,
        client_id=client_id,
    )


@router.post("/login", response_model=WhmcsLoginResponse)
async def whmcs_login(payload: WhmcsLoginRequest):
    """Authenticate a user against WHMCS and issue an app JWT.

    Flow:
      1. Call WHMCS `ValidateLogin` with email + password. On success WHMCS
         returns the client id and password hash (we don't use the hash).
      2. Fetch the client's profile via `GetClientsDetails` so we can populate
         email/name on the token (ValidateLogin itself doesn't return them).
      3. Mint an app JWT via `core.auth.create_access_token` and return it.

    The frontend stores the returned `token` in `localStorage.token`; the
    existing web-sdk then automatically attaches it as `Authorization: Bearer ...`
    on every subsequent request, so `/api/v1/auth/me` and all `/api/v1/whmcs/*`
    endpoints will work without any further wiring.
    """
    email = payload.email.strip().lower()

    try:
        result = await call_whmcs(
            "ValidateLogin",
            {"email": email, "password2": payload.password},
        )
    except HTTPException as exc:
        # WHMCS returns result=error for bad credentials; normalize to 401
        # regardless of the specific upstream message so we don't leak details.
        # (`call_whmcs` already classifies well-known "invalid credentials"
        # messages as 401; a remaining 400 is treated as credential-level too.)
        if exc.status_code == 400:
            raise HTTPException(
                status_code=401,
                detail="Invalid email or password.",
            ) from exc
        # 401 / 502 / 500 from call_whmcs already carry the right message
        # (invalid credentials vs WHMCS IP not whitelisted vs config missing).
        raise

    client_id = _safe_int(result.get("userid") or result.get("clientid"))
    if client_id <= 0:
        raise HTTPException(status_code=401, detail="Invalid email or password.")

    # Fetch client profile to populate token name/email fields.
    client = await get_client_by_email(email)
    name: Optional[str] = None
    resolved_email = email
    if client:
        name = (
            (client.get("fullname") or "").strip()
            or f"{client.get('firstname') or ''} {client.get('lastname') or ''}".strip()
            or None
        )
        resolved_email = (client.get("email") or email).lower()

    token = await _build_app_token(client_id=client_id, email=resolved_email, name=name)
    return WhmcsLoginResponse(
        token=token,
        user_id=f"whmcs:{client_id}",
        email=resolved_email,
        name=name,
        client_id=client_id,
    )


@router.get("/me", response_model=WhmcsProfile)
async def whmcs_me(current_user: UserResponse = Depends(get_current_user)):
    """Return the WHMCS profile for the currently signed-in user (looked up by email).

    If WHMCS is temporarily unreachable we degrade gracefully and return a minimal
    profile built from the JWT claims instead of a hard 5xx, so the dashboard
    shell can still render while the user retries.
    """
    if not current_user.email:
        raise HTTPException(status_code=400, detail="Signed-in user has no email on file.")

    try:
        client = await get_client_by_email(current_user.email)
    except HTTPException as exc:
        # Transport / upstream 5xx — return a degraded profile instead of 502
        # so Cloudflare doesn't see an edge timeout and the dashboard still loads.
        if exc.status_code >= 500:
            logger.warning(
                "WHMCS upstream unavailable for /me; returning degraded profile: %s",
                exc.detail,
            )
            return WhmcsProfile(
                client_id=0,
                firstname="",
                lastname="",
                fullname=current_user.name or current_user.email,
                email=current_user.email,
            )
        raise

    if not client:
        raise HTTPException(
            status_code=404,
            detail="No WHMCS client linked to this account. Please complete registration.",
        )
    stats = client.get("statistics") or {}

    return WhmcsProfile(
        client_id=_safe_int(client.get("userid") or client.get("id")),
        firstname=str(client.get("firstname") or ""),
        lastname=str(client.get("lastname") or ""),
        fullname=str(client.get("fullname") or ""),
        email=str(client.get("email") or current_user.email),
        company=client.get("companyname") or None,
        address1=client.get("address1") or None,
        address2=client.get("address2") or None,
        city=client.get("city") or None,
        state=client.get("state") or None,
        postcode=client.get("postcode") or None,
        country=client.get("country") or None,
        phonenumber=client.get("phonenumber") or None,
        status=client.get("status") or None,
        credit=str(client.get("credit")) if client.get("credit") is not None else None,
        currency_code=client.get("currency_code") or client.get("currency") or None,
        num_invoices=_safe_int(stats.get("numinvoices"), 0) or None,
        num_due_invoices=_safe_int(stats.get("numduesinvoices") or stats.get("numdueinvoices"), 0) or None,
        num_products=_safe_int(stats.get("numactiveproducts") or stats.get("productsnumactive"), 0) or None,
        num_domains=_safe_int(stats.get("numactivedomains") or stats.get("domainsnumactive"), 0) or None,
        num_tickets=_safe_int(stats.get("numactivequotes") or stats.get("numtickets"), 0) or None,
    )


@router.get("/services", response_model=WhmcsListResponse)
async def whmcs_services(current_user: UserResponse = Depends(get_current_user)):
    if not current_user.email:
        raise HTTPException(status_code=400, detail="Signed-in user has no email on file.")
    client = await _require_whmcs_client(current_user.email)
    client_id = _safe_int(client.get("userid") or client.get("id"))

    result = await call_whmcs_safe(
        "GetClientsProducts", {"clientid": client_id, "stats": "true"}
    ) or {}
    products_wrapper = result.get("products") or {}
    raw_items = _as_list(products_wrapper.get("product"))
    items = [
        WhmcsService(
            id=_safe_int(item.get("id")),
            product_name=item.get("name") or item.get("productname") or item.get("groupname"),
            domain=item.get("domain"),
            status=item.get("status"),
            regdate=item.get("regdate"),
            nextduedate=item.get("nextduedate"),
            billingcycle=item.get("billingcycle"),
            recurringamount=str(item.get("recurringamount")) if item.get("recurringamount") is not None else None,
            paymentmethod=item.get("paymentmethod"),
        ).model_dump()
        for item in raw_items
    ]
    return WhmcsListResponse(items=items, total=_safe_int(result.get("totalresults"), len(items)))


@router.get("/invoices", response_model=WhmcsListResponse)
async def whmcs_invoices(current_user: UserResponse = Depends(get_current_user)):
    if not current_user.email:
        raise HTTPException(status_code=400, detail="Signed-in user has no email on file.")
    client = await _require_whmcs_client(current_user.email)
    client_id = _safe_int(client.get("userid") or client.get("id"))

    result = await call_whmcs_safe(
        "GetInvoices", {"userid": client_id, "limitnum": 100}
    ) or {}
    invoices_wrapper = result.get("invoices") or {}
    raw_items = _as_list(invoices_wrapper.get("invoice"))
    items = [
        WhmcsInvoice(
            id=_safe_int(item.get("id")),
            invoicenum=item.get("invoicenum") or str(item.get("id")),
            status=item.get("status"),
            date=item.get("date"),
            duedate=item.get("duedate"),
            datepaid=item.get("datepaid"),
            total=str(item.get("total")) if item.get("total") is not None else None,
            currency_code=item.get("currencycode") or item.get("currency_code"),
        ).model_dump()
        for item in raw_items
    ]
    return WhmcsListResponse(items=items, total=_safe_int(result.get("totalresults"), len(items)))


@router.get("/domains", response_model=WhmcsListResponse)
async def whmcs_domains(current_user: UserResponse = Depends(get_current_user)):
    if not current_user.email:
        raise HTTPException(status_code=400, detail="Signed-in user has no email on file.")
    client = await _require_whmcs_client(current_user.email)
    client_id = _safe_int(client.get("userid") or client.get("id"))

    result = await call_whmcs_safe(
        "GetClientsDomains", {"clientid": client_id, "limitnum": 100}
    ) or {}
    domains_wrapper = result.get("domains") or {}
    raw_items = _as_list(domains_wrapper.get("domain"))
    items = [
        WhmcsDomain(
            id=_safe_int(item.get("id")),
            domainname=item.get("domainname"),
            status=item.get("status"),
            registrar=item.get("registrar"),
            registrationdate=item.get("registrationdate"),
            expirydate=item.get("expirydate") or item.get("nextduedate"),
            recurringamount=str(item.get("recurringamount")) if item.get("recurringamount") is not None else None,
        ).model_dump()
        for item in raw_items
    ]
    return WhmcsListResponse(items=items, total=_safe_int(result.get("totalresults"), len(items)))


@router.get("/emails", response_model=WhmcsListResponse)
async def whmcs_emails(current_user: UserResponse = Depends(get_current_user)):
    """Return the email history WHMCS has sent to the signed-in client.

    This includes invoice notices, payment confirmations, service welcome
    emails, password resets, etc. Useful for letting the client re-read a
    notification they may have missed or deleted in their inbox.
    """
    if not current_user.email:
        raise HTTPException(status_code=400, detail="Signed-in user has no email on file.")
    client = await _require_whmcs_client(current_user.email)
    client_id = _safe_int(client.get("userid") or client.get("id"))

    result = await call_whmcs_safe(
        "GetEmails", {"clientid": client_id, "limitnum": 100}
    ) or {}
    emails_wrapper = result.get("emails") or {}
    raw_items = _as_list(emails_wrapper.get("email"))
    items = [
        WhmcsEmail(
            id=_safe_int(item.get("id")),
            subject=item.get("subject"),
            date=item.get("date"),
        ).model_dump()
        for item in raw_items
    ]
    return WhmcsListResponse(items=items, total=_safe_int(result.get("totalresults"), len(items)))


@router.get("/tickets", response_model=WhmcsListResponse)
async def whmcs_tickets(current_user: UserResponse = Depends(get_current_user)):
    """List tickets for the signed-in client.

    WHMCS stores the owning client id on each ticket, but some tickets can also
    be opened by email without a clientid linkage (for example, when support
    staff create them from the admin area using the customer's email). To make
    sure the portal shows every ticket the user has, we issue both queries
    *concurrently* (so total latency stays within Cloudflare's 100s origin
    budget) and we treat the secondary email-scoped query as best-effort: if
    it times out or errors we still return the primary client-scoped list so
    the page never 502s just because one call was slow.
    """
    import asyncio

    if not current_user.email:
        raise HTTPException(status_code=400, detail="Signed-in user has no email on file.")

    client = await _require_whmcs_client(current_user.email)
    client_id = _safe_int(client.get("userid") or client.get("id"))
    email = (client.get("email") or current_user.email or "").strip()

    async def _fetch(params: Dict[str, Any]) -> Dict[str, Any]:
        try:
            return await asyncio.wait_for(
                call_whmcs_safe("GetTickets", params), timeout=25.0
            ) or {}
        except (asyncio.TimeoutError, Exception) as exc:  # noqa: BLE001
            logger.warning("GetTickets query failed (%s): %s", params, exc)
            return {}

    # Run both lookups concurrently so the slowest one dominates latency,
    # not the sum. `return_exceptions=True` guarantees we never propagate
    # a transport error from the email-scoped lookup as a 500/502.
    by_client, by_email = await asyncio.gather(
        _fetch({"clientid": client_id, "limitnum": 100}),
        _fetch({"email": email, "limitnum": 100}),
        return_exceptions=False,
    )

    merged: Dict[int, Dict[str, Any]] = {}
    for wrapper in (by_client, by_email):
        if not isinstance(wrapper, dict):
            continue
        raw_items = _as_list((wrapper.get("tickets") or {}).get("ticket"))
        for item in raw_items:
            tid_int = _safe_int(item.get("id"))
            if tid_int <= 0:
                continue
            # First-write wins; both responses carry the same fields.
            merged.setdefault(tid_int, item)

    # Sort newest first by lastreply date when available, then by id desc.
    def _sort_key(rec: Dict[str, Any]) -> tuple:
        return (str(rec.get("lastreply") or ""), _safe_int(rec.get("id")))

    ordered = sorted(merged.values(), key=_sort_key, reverse=True)

    items = [
        WhmcsTicket(
            id=_safe_int(item.get("id")),
            tid=item.get("tid"),
            subject=item.get("subject"),
            status=item.get("status"),
            priority=item.get("priority") or item.get("urgency"),
            department=item.get("deptname") or item.get("department"),
            lastreply=item.get("lastreply"),
        ).model_dump()
        for item in ordered
    ]
    return WhmcsListResponse(items=items, total=len(items))


@router.get("/tickets/{ticket_id}", response_model=WhmcsTicketDetail)
async def whmcs_ticket_detail(
    ticket_id: int,
    current_user: UserResponse = Depends(get_current_user),
):
    """Return a single ticket with all its replies.

    The initial ticket description is returned as the first message in
    ``messages``; subsequent replies follow in WHMCS order.
    """
    if not current_user.email:
        raise HTTPException(status_code=400, detail="Signed-in user has no email on file.")
    client = await _require_whmcs_client(current_user.email)
    client_id = _safe_int(client.get("userid") or client.get("id"))

    result = await call_whmcs("GetTicket", {"ticketid": ticket_id})

    # Authorisation check — make sure this ticket actually belongs to the
    # signed-in client. WHMCS returns userid / requestor_id on the ticket.
    ticket_owner = _safe_int(result.get("userid") or result.get("requestor_id"))
    if ticket_owner > 0 and ticket_owner != client_id:
        raise HTTPException(status_code=404, detail="Ticket not found.")

    messages: List[WhmcsTicketReply] = []

    # Initial message (the ticket body itself).
    initial_body = result.get("message") or result.get("body")
    if initial_body:
        messages.append(
            WhmcsTicketReply(
                name=result.get("name") or result.get("requestor_name") or current_user.email,
                email=result.get("email") or current_user.email,
                date=result.get("date"),
                message=str(initial_body),
                is_staff=False,
                attachments=[],
            )
        )

    # Follow-up replies.
    replies_wrapper = result.get("replies") or {}
    for raw in _as_list(replies_wrapper.get("reply")):
        admin_name = raw.get("admin") or raw.get("adminusername")
        is_staff = bool(admin_name and str(admin_name).strip())
        messages.append(
            WhmcsTicketReply(
                id=_safe_int(raw.get("id")) or None,
                name=admin_name or raw.get("name") or raw.get("email") or "Client",
                email=raw.get("email"),
                date=raw.get("date"),
                message=raw.get("message") or "",
                is_staff=is_staff,
                attachments=[],
            )
        )

    return WhmcsTicketDetail(
        id=_safe_int(result.get("id") or ticket_id),
        tid=result.get("tid"),
        subject=result.get("subject"),
        status=result.get("status"),
        priority=result.get("priority") or result.get("urgency"),
        department=result.get("deptname") or result.get("department"),
        department_id=_safe_int(result.get("deptid") or result.get("departmentid"), 0) or None,
        date=result.get("date"),
        lastreply=result.get("lastreply"),
        messages=messages,
    )


@router.get("/support-departments", response_model=WhmcsDepartmentsResponse)
async def whmcs_support_departments(current_user: UserResponse = Depends(get_current_user)):
    """Return the list of WHMCS support departments available for opening a ticket."""
    # current_user just enforces auth; the department list itself is the same for all clients.
    _ = current_user
    result = await call_whmcs("GetSupportDepartments", {})
    departments_wrapper = result.get("departments") or {}
    raw_items = _as_list(departments_wrapper.get("department"))
    items = [
        WhmcsDepartment(
            id=_safe_int(item.get("id")),
            name=str(item.get("name") or f"Department {item.get('id')}"),
        )
        for item in raw_items
        if _safe_int(item.get("id")) > 0
    ]
    return WhmcsDepartmentsResponse(items=items)


@router.post("/tickets", response_model=WhmcsCreateTicketResponse)
async def whmcs_create_ticket(
    payload: WhmcsCreateTicketRequest,
    current_user: UserResponse = Depends(get_current_user),
):
    """Open a new support ticket for the signed-in client.

    Uses WHMCS ``OpenTicket`` with the client id so the ticket is attached to
    the correct account automatically.
    """
    if not current_user.email:
        raise HTTPException(status_code=400, detail="Signed-in user has no email on file.")
    client = await _require_whmcs_client(current_user.email)
    client_id = _safe_int(client.get("userid") or client.get("id"))

    # Normalise priority — WHMCS accepts Low / Medium / High.
    priority = (payload.priority or "Medium").strip().title()
    if priority not in {"Low", "Medium", "High"}:
        priority = "Medium"

    result = await call_whmcs(
        "OpenTicket",
        {
            "clientid": client_id,
            "deptid": payload.department_id,
            "subject": payload.subject,
            "message": payload.message,
            "priority": priority,
        },
    )
    return WhmcsCreateTicketResponse(
        id=_safe_int(result.get("id") or result.get("ticketid")),
        tid=result.get("tid"),
    )


@router.post("/tickets/{ticket_id}/reply", response_model=WhmcsReplyTicketResponse)
async def whmcs_reply_ticket(
    ticket_id: int,
    payload: WhmcsReplyTicketRequest,
    current_user: UserResponse = Depends(get_current_user),
):
    """Post a reply to an existing ticket on behalf of the signed-in client."""
    if not current_user.email:
        raise HTTPException(status_code=400, detail="Signed-in user has no email on file.")
    client = await _require_whmcs_client(current_user.email)
    client_id = _safe_int(client.get("userid") or client.get("id"))

    # Authorisation: make sure the ticket belongs to this client before replying.
    existing = await call_whmcs("GetTicket", {"ticketid": ticket_id})
    ticket_owner = _safe_int(existing.get("userid") or existing.get("requestor_id"))
    if ticket_owner > 0 and ticket_owner != client_id:
        raise HTTPException(status_code=404, detail="Ticket not found.")

    await call_whmcs(
        "AddTicketReply",
        {
            "ticketid": ticket_id,
            "clientid": client_id,
            "message": payload.message,
        },
    )
    return WhmcsReplyTicketResponse()


@router.post("/update-profile", response_model=WhmcsUpdateProfileResponse)
async def whmcs_update_profile(
    payload: WhmcsUpdateProfileRequest,
    current_user: UserResponse = Depends(get_current_user),
):
    if not current_user.email:
        raise HTTPException(status_code=400, detail="Signed-in user has no email on file.")
    client = await _require_whmcs_client(current_user.email)
    client_id = _safe_int(client.get("userid") or client.get("id"))

    update_fields: Dict[str, Any] = {"clientid": client_id}
    mapping = {
        "firstname": payload.firstname,
        "lastname": payload.lastname,
        "companyname": payload.companyname,
        "address1": payload.address1,
        "address2": payload.address2,
        "city": payload.city,
        "state": payload.state,
        "postcode": payload.postcode,
        "country": payload.country.upper() if payload.country else None,
        "phonenumber": payload.phonenumber,
    }
    for key, value in mapping.items():
        if value is not None:
            update_fields[key] = value

    if len(update_fields) <= 1:
        raise HTTPException(status_code=400, detail="No fields to update.")

    await call_whmcs("UpdateClient", update_fields)
    # Drop cached GetClientsDetails so the next /me reflects the edit.
    invalidate_client_cache(current_user.email)
    return WhmcsUpdateProfileResponse(success=True)


# ---------------------------------------------------------------------------
# Domain availability search — PUBLIC endpoint (no auth required).
# Uses WHMCS `DomainWhois` to check real-time availability, combined with
# optional `DomainGetPrices` for per-TLD pricing.
# ---------------------------------------------------------------------------
class DomainCheckRequest(BaseModel):
    domain: str = Field(..., min_length=1, max_length=253)
    tlds: Optional[List[str]] = Field(
        default=None,
        description="List of TLDs to check without the dot, e.g. ['com','net','org']. If omitted a default set is used.",
    )


class DomainAvailabilityItem(BaseModel):
    domain: str
    tld: str
    available: bool
    status: str  # 'available' | 'taken' | 'unknown'
    price: Optional[str] = None
    currency: Optional[str] = None
    register_url: Optional[str] = None


class DomainCheckResponse(BaseModel):
    sld: str
    results: List[DomainAvailabilityItem]


_DEFAULT_TLDS = ["com", "net", "org", "info", "io", "co", "sa", "ae", "online", "store"]


def _sanitise_sld(raw: str) -> str:
    """Extract the second-level part of a user-entered domain.

    Accepts forms like 'mysite', 'mysite.com', 'https://mysite.com/path'
    and returns the bare SLD ('mysite'). Keeps only a-z, 0-9, and hyphen.
    """
    value = (raw or "").strip().lower()
    # strip scheme and path
    if "://" in value:
        value = value.split("://", 1)[1]
    value = value.split("/", 1)[0].strip()
    # drop any TLD the user typed so we can recombine with each requested TLD
    if "." in value:
        value = value.split(".", 1)[0]
    # sanitise
    cleaned = "".join(ch for ch in value if ch.isalnum() or ch == "-").strip("-")
    return cleaned


async def _whmcs_domain_prices() -> Dict[str, Dict[str, Any]]:
    """Return a map of tld -> {'register': price, 'currency': code}.

    Uses WHMCS ``DomainGetPrices``. Safe: returns empty dict on any failure so
    availability results can still render without pricing.
    """
    raw = await call_whmcs_safe("DomainGetPrices", {}) or {}
    prices_root = raw.get("prices") or {}
    out: Dict[str, Dict[str, Any]] = {}
    currency = raw.get("currency") or {}
    currency_code = (
        currency.get("code")
        if isinstance(currency, dict)
        else None
    )
    for tld_key, info in prices_root.items():
        if not isinstance(info, dict):
            continue
        register = info.get("register") or {}
        # WHMCS returns the 1-year registration price under key '1'.
        first_year = None
        if isinstance(register, dict):
            first_year = register.get("1") or next(iter(register.values()), None)
        tld_clean = str(tld_key).lstrip(".").lower()
        out[tld_clean] = {
            "register": str(first_year) if first_year is not None else None,
            "currency": currency_code,
        }
    return out


@router.post("/domain-check", response_model=DomainCheckResponse)
async def whmcs_domain_check(payload: DomainCheckRequest):
    """Check availability for a domain across one or more TLDs.

    Public endpoint — no authentication required. Calls WHMCS ``DomainWhois``
    once per TLD and merges with ``DomainGetPrices`` for display pricing.
    """
    sld = _sanitise_sld(payload.domain)
    if not sld or len(sld) < 2:
        raise HTTPException(
            status_code=400,
            detail="Please enter a valid domain name (at least 2 characters).",
        )

    tlds = [
        t.strip().lstrip(".").lower()
        for t in (payload.tlds or _DEFAULT_TLDS)
        if t and t.strip()
    ][:12]  # hard cap to keep latency predictable

    prices = await _whmcs_domain_prices()

    results: List[DomainAvailabilityItem] = []
    for tld in tlds:
        full_domain = f"{sld}.{tld}"
        whois = await call_whmcs_safe("DomainWhois", {"domain": full_domain}) or {}
        status_code = str(whois.get("status") or "").strip().lower()
        # WHMCS status values: 'available' or 'unavailable' (and 'invalid' on bad TLDs)
        if status_code == "available":
            available = True
            status = "available"
        elif status_code in {"unavailable", "taken"}:
            available = False
            status = "taken"
        else:
            available = False
            status = "unknown"

        price_info = prices.get(tld) or {}
        results.append(
            DomainAvailabilityItem(
                domain=full_domain,
                tld=tld,
                available=available,
                status=status,
                price=price_info.get("register"),
                currency=price_info.get("currency"),
                register_url=f"/hosting?domain={full_domain}" if available else None,
            )
        )

    return DomainCheckResponse(sld=sld, results=results)


# ---------------------------------------------------------------------------
# Password reset — intentionally does NOT require an authenticated user.
# We always respond with a generic success message to avoid email enumeration.
# ---------------------------------------------------------------------------
@router.post("/forgot-password", response_model=WhmcsForgotPasswordResponse)
async def whmcs_forgot_password(payload: WhmcsForgotPasswordRequest):
    """Initiate a password reset.

    Flow:
      1) Look up the WHMCS client by email (if any).
      2) Mint a signed 30-minute reset token using the app JWT secret.
      3) If a WHMCS client exists, ask WHMCS to email them the reset link
         using its own SMTP (SendEmail / customtype=general).
      4) Always respond with the same generic message to prevent leaking which
         emails exist in the system.
    """
    email = payload.email.strip().lower()
    try:
        client = await get_client_by_email(email)
    except HTTPException:
        # Upstream config error — still respond generically so we don't leak info.
        logger.exception("WHMCS lookup failed during forgot-password for email_hash")
        client = None

    client_id = 0
    if client:
        try:
            client_id = int(client.get("userid") or client.get("id") or 0)
        except (TypeError, ValueError):
            client_id = 0

    # Always mint a token (so response timing is uniform), but only send the
    # email when we actually found a WHMCS client to deliver it to.
    token = create_reset_token(email=email, client_id=client_id or None)
    reset_link = build_reset_link(token)

    if client_id > 0:
        sent = await send_reset_email_via_whmcs(client_id=client_id, reset_link=reset_link)
        if not sent:
            logger.warning(
                "Password reset email could not be delivered via WHMCS for client_id=%s",
                client_id,
            )
    else:
        logger.info(
            "Password reset requested for an email with no linked WHMCS client; "
            "responding generically."
        )

    return WhmcsForgotPasswordResponse()


@router.post("/reset-password", response_model=WhmcsResetPasswordResponse)
async def whmcs_reset_password(payload: WhmcsResetPasswordRequest):
    """Complete a password reset.

    - Verifies the signed reset token (issued by Atoms-backed auth secret).
    - Calls WHMCS UpdateClient with `password2` to sync the new password to
      the linked WHMCS client, so they can sign in to both the portal and
      WHMCS-native areas with the same credentials.
    """
    claims = verify_reset_token(payload.token)
    email = str(claims.get("sub") or "").strip().lower()
    claim_client_id = _safe_int(claims.get("whmcs_client_id"), 0)

    # Re-resolve the WHMCS client from the email at reset time in case the
    # client id changed or the account was relinked.
    client = await get_client_by_email(email)
    client_id = _safe_int(
        (client or {}).get("userid") or (client or {}).get("id"), 0
    ) or claim_client_id

    whmcs_synced = False
    if client_id > 0:
        whmcs_synced = await sync_password_to_whmcs(
            client_id=client_id, new_password=payload.new_password
        )
        if not whmcs_synced:
            raise HTTPException(
                status_code=502,
                detail=(
                    "We could not update your password in the billing system. "
                    "Please try again or contact support."
                ),
            )
    else:
        # No WHMCS record linked; the reset token was still valid but there is
        # nothing to sync. Tell the caller so the UI can show a clear message.
        logger.info(
            "Password reset completed for email with no linked WHMCS client."
        )

    return WhmcsResetPasswordResponse(whmcs_synced=whmcs_synced)