# @File: backend/services/whatsapp/chat.py
# @Desc: Client-side chat service — conversation list + message thread CRUD
#        over the `whatsapp_messages` / `whatsapp_contacts` tables. Keeps the
#        surface minimal (what the WhatsApp-style UI needs) so it can run
#        before the upstream-send pipeline is wired. Outbound messages are
#        persisted as `delivery_status="queued"` so a later dispatcher can
#        pick them up without schema changes.
from __future__ import annotations

import logging
import re
from datetime import datetime
from typing import Optional

from models.whatsapp import (
    WhatsappClientSubscription,
    WhatsappContact,
    WhatsappDevice,
    WhatsappMessage,
)
from sqlalchemy import and_, desc, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession

logger = logging.getLogger(__name__)

# Max rows returned in a single call. Keeps response payloads predictable
# and protects the DB from pathological clients.
CHAT_LIST_LIMIT = 100
MESSAGE_PAGE_LIMIT = 200

# Match E.164-style or locally-formatted phone numbers. We deliberately keep
# validation permissive — WhatsApp numbers include country prefixes written in
# several forms and the upstream server is the source of truth.
_PHONE_RE = re.compile(r"^[+\d][\d\s\-()]{5,30}$")


def _normalize_phone(phone: str) -> str:
    """Strip whitespace/dashes/parens but keep a leading ``+`` if present."""
    if not phone:
        return ""
    cleaned = re.sub(r"[\s\-()]", "", phone.strip())
    return cleaned


def is_valid_phone(phone: str) -> bool:
    return bool(phone) and bool(_PHONE_RE.match(phone.strip()))


async def _get_active_subscription(
    db: AsyncSession, user_id: str
) -> Optional[WhatsappClientSubscription]:
    """Return the most recently-activated ACTIVE subscription for ``user_id``.

    The UI is currently single-subscription; if the user owns multiple we pick
    the newest active one. Suspended / terminated subs are excluded.
    """
    stmt = (
        select(WhatsappClientSubscription)
        .where(
            and_(
                WhatsappClientSubscription.user_id == user_id,
                WhatsappClientSubscription.status == "active",
            )
        )
        .order_by(desc(WhatsappClientSubscription.activated_at))
        .limit(1)
    )
    result = await db.execute(stmt)
    return result.scalar_one_or_none()


async def _get_default_device(
    db: AsyncSession, subscription_id: int
) -> Optional[WhatsappDevice]:
    """Return the first non-archived connected device, falling back to any
    non-archived device. Used when the caller doesn't pin a device."""
    stmt = (
        select(WhatsappDevice)
        .where(
            and_(
                WhatsappDevice.subscription_id == subscription_id,
                WhatsappDevice.archived.is_(False),
            )
        )
        .order_by(
            # connected first, then most recently seen.
            (WhatsappDevice.status == "connected").desc(),
            desc(WhatsappDevice.last_seen_at),
            desc(WhatsappDevice.id),
        )
        .limit(1)
    )
    result = await db.execute(stmt)
    return result.scalar_one_or_none()


async def _find_or_create_contact(
    db: AsyncSession,
    *,
    subscription_id: int,
    user_id: str,
    phone_number: str,
    display_name: Optional[str] = None,
) -> WhatsappContact:
    normalized = _normalize_phone(phone_number)
    stmt = select(WhatsappContact).where(
        and_(
            WhatsappContact.subscription_id == subscription_id,
            WhatsappContact.phone_number == normalized,
        )
    )
    contact = (await db.execute(stmt)).scalar_one_or_none()
    if contact:
        if display_name and not contact.display_name:
            contact.display_name = display_name
        return contact

    contact = WhatsappContact(
        subscription_id=subscription_id,
        user_id=user_id,
        phone_number=normalized,
        display_name=display_name,
    )
    db.add(contact)
    await db.flush()
    return contact


async def list_conversations(
    db: AsyncSession,
    *,
    user_id: str,
    unread_only: bool = False,
    search: Optional[str] = None,
) -> list[dict]:
    """Return one row per contact with last-message + unread-count summary.

    The WhatsApp UI shows a scrolling list of chats; each row needs:
      - contact info (id, phone_number, display_name, is_opted_out, tags)
      - last message preview + direction + delivery_status + timestamp
      - unread inbound-message count (direction=inbound, is_read=false)
    """
    subscription = await _get_active_subscription(db, user_id)
    if not subscription:
        return []

    # Per-contact last-message id
    last_msg_subq = (
        select(
            WhatsappMessage.contact_id.label("contact_id"),
            func.max(WhatsappMessage.id).label("last_id"),
        )
        .where(
            and_(
                WhatsappMessage.subscription_id == subscription.id,
                WhatsappMessage.contact_id.is_not(None),
            )
        )
        .group_by(WhatsappMessage.contact_id)
        .subquery()
    )

    # Per-contact unread count (only inbound)
    unread_subq = (
        select(
            WhatsappMessage.contact_id.label("contact_id"),
            func.count(WhatsappMessage.id).label("unread_count"),
        )
        .where(
            and_(
                WhatsappMessage.subscription_id == subscription.id,
                WhatsappMessage.contact_id.is_not(None),
                WhatsappMessage.direction == "inbound",
                WhatsappMessage.is_read.is_(False),
            )
        )
        .group_by(WhatsappMessage.contact_id)
        .subquery()
    )

    stmt = (
        select(
            WhatsappContact,
            WhatsappMessage,
            func.coalesce(unread_subq.c.unread_count, 0).label("unread_count"),
        )
        .join(
            last_msg_subq,
            last_msg_subq.c.contact_id == WhatsappContact.id,
            isouter=True,
        )
        .join(
            WhatsappMessage,
            WhatsappMessage.id == last_msg_subq.c.last_id,
            isouter=True,
        )
        .join(
            unread_subq,
            unread_subq.c.contact_id == WhatsappContact.id,
            isouter=True,
        )
        .where(WhatsappContact.subscription_id == subscription.id)
    )

    if search:
        pattern = f"%{search.strip()}%"
        stmt = stmt.where(
            or_(
                WhatsappContact.phone_number.ilike(pattern),
                WhatsappContact.display_name.ilike(pattern),
            )
        )

    if unread_only:
        stmt = stmt.where(func.coalesce(unread_subq.c.unread_count, 0) > 0)

    stmt = stmt.order_by(
        desc(func.coalesce(WhatsappContact.last_message_at, WhatsappContact.created_at))
    ).limit(CHAT_LIST_LIMIT)

    rows = (await db.execute(stmt)).all()

    conversations: list[dict] = []
    for contact, last_msg, unread_count in rows:
        last_preview = None
        if last_msg is not None:
            last_preview = {
                "id": last_msg.id,
                "direction": last_msg.direction,
                "message_type": last_msg.message_type,
                "body": last_msg.body,
                "delivery_status": last_msg.delivery_status,
                "created_at": last_msg.created_at.isoformat() if last_msg.created_at else None,
            }

        conversations.append(
            {
                "contact_id": contact.id,
                "phone_number": contact.phone_number,
                "display_name": contact.display_name,
                "tags": contact.tags,
                "is_opted_out": contact.is_opted_out,
                "last_message_at": (
                    contact.last_message_at.isoformat()
                    if contact.last_message_at
                    else None
                ),
                "unread_count": int(unread_count or 0),
                "last_message": last_preview,
            }
        )

    return conversations


async def list_messages(
    db: AsyncSession,
    *,
    user_id: str,
    contact_id: int,
    limit: int = MESSAGE_PAGE_LIMIT,
    before_id: Optional[int] = None,
) -> list[dict]:
    """Return the most-recent ``limit`` messages for a contact, ascending by id."""
    subscription = await _get_active_subscription(db, user_id)
    if not subscription:
        return []

    # Verify the contact belongs to this subscription.
    contact_stmt = select(WhatsappContact).where(
        and_(
            WhatsappContact.id == contact_id,
            WhatsappContact.subscription_id == subscription.id,
        )
    )
    contact = (await db.execute(contact_stmt)).scalar_one_or_none()
    if not contact:
        return []

    safe_limit = max(1, min(int(limit or MESSAGE_PAGE_LIMIT), MESSAGE_PAGE_LIMIT))

    conditions = [
        WhatsappMessage.subscription_id == subscription.id,
        WhatsappMessage.contact_id == contact_id,
    ]
    if before_id is not None:
        conditions.append(WhatsappMessage.id < before_id)

    stmt = (
        select(WhatsappMessage)
        .where(and_(*conditions))
        .order_by(desc(WhatsappMessage.id))
        .limit(safe_limit)
    )
    rows = (await db.execute(stmt)).scalars().all()
    # Flip to ascending — the UI renders oldest-first.
    rows = list(reversed(rows))

    return [_serialize_message(m) for m in rows]


def _serialize_message(msg: WhatsappMessage) -> dict:
    return {
        "id": msg.id,
        "contact_id": msg.contact_id,
        "device_id": msg.device_id,
        "direction": msg.direction,
        "message_type": msg.message_type,
        "body": msg.body,
        "media_url": msg.media_url,
        "media_mime": msg.media_mime,
        "delivery_status": msg.delivery_status,
        "is_read": msg.is_read,
        "created_at": msg.created_at.isoformat() if msg.created_at else None,
    }


async def send_outbound_message(
    db: AsyncSession,
    *,
    user_id: str,
    contact_id: Optional[int],
    phone_number: Optional[str],
    body: Optional[str],
    message_type: str = "text",
    media_url: Optional[str] = None,
    media_mime: Optional[str] = None,
) -> Optional[dict]:
    """Persist an outbound message as ``queued`` and return it.

    The upstream-send call is intentionally not wired here — a later
    dispatcher will pick up ``queued`` rows and promote them to ``sent``
    after the proxy call succeeds.
    """
    subscription = await _get_active_subscription(db, user_id)
    if not subscription:
        return None

    if message_type not in {"text", "image", "video", "audio", "document"}:
        message_type = "text"

    has_body = bool(body and body.strip())
    has_media = bool(media_url and media_url.strip())
    if not has_body and not has_media:
        return None

    contact: Optional[WhatsappContact] = None
    if contact_id is not None:
        contact_stmt = select(WhatsappContact).where(
            and_(
                WhatsappContact.id == contact_id,
                WhatsappContact.subscription_id == subscription.id,
            )
        )
        contact = (await db.execute(contact_stmt)).scalar_one_or_none()
        if not contact:
            return None
    elif phone_number and is_valid_phone(phone_number):
        contact = await _find_or_create_contact(
            db,
            subscription_id=subscription.id,
            user_id=user_id,
            phone_number=phone_number,
        )
    else:
        return None

    if contact.is_opted_out:
        # Surface as a failed row so the UI can display the block reason.
        msg = WhatsappMessage(
            subscription_id=subscription.id,
            contact_id=contact.id,
            user_id=user_id,
            direction="outbound",
            message_type=message_type,
            body=body,
            media_url=media_url,
            media_mime=media_mime,
            delivery_status="failed",
            error_detail="contact_opted_out",
            is_read=True,
        )
        db.add(msg)
        await db.flush()
        await db.commit()
        serialized = _serialize_message(msg)
        await _safe_publish_message(subscription.id, serialized)
        return serialized

    device = await _get_default_device(db, subscription.id)

    now = datetime.utcnow()
    msg = WhatsappMessage(
        subscription_id=subscription.id,
        device_id=device.id if device else None,
        contact_id=contact.id,
        user_id=user_id,
        direction="outbound",
        message_type=message_type,
        body=body,
        media_url=media_url,
        media_mime=media_mime,
        delivery_status="queued",
        is_read=True,
    )
    db.add(msg)
    contact.last_message_at = now
    await db.flush()
    await db.commit()

    serialized = _serialize_message(msg)
    await _safe_publish_message(subscription.id, serialized)
    return serialized


async def _safe_publish_message(subscription_id: int, message: dict) -> None:
    """Broadcast a message to subscribers of the presence hub without letting
    a hub failure take down the HTTP request that produced the message."""
    try:
        from services.whatsapp.presence import presence_hub

        await presence_hub.publish_message(subscription_id, message)
    except Exception:  # noqa: BLE001
        import logging as _logging

        _logging.getLogger(__name__).debug(
            "presence_hub publish_message failed", exc_info=True
        )


async def mark_conversation_read(
    db: AsyncSession,
    *,
    user_id: str,
    contact_id: int,
) -> int:
    """Mark every inbound message on this contact as read. Returns the
    number of rows updated."""
    subscription = await _get_active_subscription(db, user_id)
    if not subscription:
        return 0

    stmt = select(WhatsappMessage).where(
        and_(
            WhatsappMessage.subscription_id == subscription.id,
            WhatsappMessage.contact_id == contact_id,
            WhatsappMessage.direction == "inbound",
            WhatsappMessage.is_read.is_(False),
        )
    )
    rows = (await db.execute(stmt)).scalars().all()
    for row in rows:
        row.is_read = True
    if rows:
        await db.commit()
    return len(rows)