# @File: backend/services/whatsapp/campaigns.py
# @Desc: Campaigns (bulk messaging) service for the WhatsApp reseller
#        module. Owns draft CRUD, recipient import, scheduling, and
#        lifecycle transitions (start / pause / resume / cancel). We keep
#        all recipients in ``whatsapp_campaign_recipients`` and cache
#        counters on the parent campaign row so dashboards don't need to
#        aggregate on every render.
#
#        Actual upstream delivery is intentionally NOT handled here — a
#        separate dispatcher will pick up rows with status="queued"
#        later. That keeps the service layer testable without a live
#        WhatsApp gateway, and lets us ship the client UI first.
from __future__ import annotations

import logging
import re
from datetime import datetime, timezone
from typing import Optional

from fastapi import HTTPException, status
from models.whatsapp import (
    WhatsappCampaign,
    WhatsappCampaignRecipient,
    WhatsappClientSubscription,
)
from sqlalchemy import and_, desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession

from services.whatsapp.chat import _get_active_subscription, _normalize_phone

logger = logging.getLogger(__name__)

CAMPAIGN_LIST_LIMIT = 100
RECIPIENT_LIST_LIMIT = 500

# Recipients are capped so a buggy CSV import can't OOM the process. The
# cap is deliberately generous — reseller plans usually top out below this.
MAX_RECIPIENTS_PER_CAMPAIGN = 50_000


_PHONE_RE = re.compile(r"^\+?\d[\d\s\-()]{5,30}$")


async def _require_subscription(db: AsyncSession, user_id: str) -> WhatsappClientSubscription:
    sub = await _get_active_subscription(db, user_id)
    if sub is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="No active WhatsApp subscription found for this account.",
        )
    return sub


def _serialize(campaign: WhatsappCampaign) -> dict:
    return {
        "id": campaign.id,
        "name": campaign.name,
        "message_body": campaign.message_body,
        "media_url": campaign.media_url,
        "status": campaign.status,
        "scheduled_at": campaign.scheduled_at,
        "started_at": campaign.started_at,
        "completed_at": campaign.completed_at,
        "total_recipients": campaign.total_recipients,
        "sent_count": campaign.sent_count,
        "delivered_count": campaign.delivered_count,
        "replied_count": campaign.replied_count,
        "failed_count": campaign.failed_count,
        "throttle_per_minute": campaign.throttle_per_minute,
        "last_error": campaign.last_error,
        "created_at": campaign.created_at,
        "updated_at": campaign.updated_at,
    }


def _serialize_recipient(row: WhatsappCampaignRecipient) -> dict:
    return {
        "id": row.id,
        "phone_number": row.phone_number,
        "contact_id": row.contact_id,
        "status": row.status,
        "error_detail": row.error_detail,
        "sent_at": row.sent_at,
    }


def _dedupe_recipients(items: list[dict]) -> list[dict]:
    """Normalize phone numbers and drop duplicates within a single payload.

    The first occurrence wins — later rows with the same phone number are
    discarded so a sloppy paste doesn't produce repeated sends.
    """
    seen: set[str] = set()
    cleaned: list[dict] = []
    for raw in items:
        phone = _normalize_phone(str(raw.get("phone_number") or ""))
        if not phone or not _PHONE_RE.match(phone):
            continue
        if phone in seen:
            continue
        seen.add(phone)
        cleaned.append(
            {
                "phone_number": phone,
                "contact_id": raw.get("contact_id"),
            }
        )
    return cleaned


async def _load_campaign(
    db: AsyncSession, *, campaign_id: int, subscription_id: int
) -> WhatsappCampaign:
    stmt = select(WhatsappCampaign).where(
        and_(
            WhatsappCampaign.id == campaign_id,
            WhatsappCampaign.subscription_id == subscription_id,
        )
    )
    row = (await db.execute(stmt)).scalar_one_or_none()
    if row is None:
        raise HTTPException(status_code=404, detail="Campaign not found.")
    return row


async def list_campaigns(db: AsyncSession, user_id: str) -> list[dict]:
    sub = await _require_subscription(db, user_id)
    stmt = (
        select(WhatsappCampaign)
        .where(WhatsappCampaign.subscription_id == sub.id)
        .order_by(desc(WhatsappCampaign.created_at))
        .limit(CAMPAIGN_LIST_LIMIT)
    )
    rows = (await db.execute(stmt)).scalars().all()
    return [_serialize(r) for r in rows]


async def get_campaign(db: AsyncSession, user_id: str, campaign_id: int) -> dict:
    sub = await _require_subscription(db, user_id)
    row = await _load_campaign(db, campaign_id=campaign_id, subscription_id=sub.id)
    return _serialize(row)


async def list_recipients(
    db: AsyncSession, user_id: str, campaign_id: int, limit: int = RECIPIENT_LIST_LIMIT
) -> list[dict]:
    sub = await _require_subscription(db, user_id)
    await _load_campaign(db, campaign_id=campaign_id, subscription_id=sub.id)
    limit = max(1, min(limit, RECIPIENT_LIST_LIMIT))
    stmt = (
        select(WhatsappCampaignRecipient)
        .where(WhatsappCampaignRecipient.campaign_id == campaign_id)
        .order_by(WhatsappCampaignRecipient.id)
        .limit(limit)
    )
    rows = (await db.execute(stmt)).scalars().all()
    return [_serialize_recipient(r) for r in rows]


async def create_campaign(db: AsyncSession, user_id: str, payload: dict) -> dict:
    sub = await _require_subscription(db, user_id)

    recipients = _dedupe_recipients(list(payload.get("recipients") or []))
    if len(recipients) > MAX_RECIPIENTS_PER_CAMPAIGN:
        raise HTTPException(
            status_code=400,
            detail=f"Too many recipients (max {MAX_RECIPIENTS_PER_CAMPAIGN}).",
        )

    campaign = WhatsappCampaign(
        subscription_id=sub.id,
        user_id=user_id,
        name=str(payload.get("name") or "").strip(),
        message_body=str(payload.get("message_body") or "").strip(),
        media_url=(payload.get("media_url") or None),
        scheduled_at=payload.get("scheduled_at"),
        throttle_per_minute=int(payload.get("throttle_per_minute") or 30),
        status="scheduled" if payload.get("scheduled_at") else "draft",
        total_recipients=len(recipients),
    )
    db.add(campaign)
    await db.flush()  # need campaign.id for recipients

    for r in recipients:
        db.add(
            WhatsappCampaignRecipient(
                campaign_id=campaign.id,
                contact_id=r.get("contact_id"),
                phone_number=r["phone_number"],
                status="pending",
            )
        )

    await db.commit()
    await db.refresh(campaign)
    return _serialize(campaign)


async def update_campaign(
    db: AsyncSession, user_id: str, campaign_id: int, payload: dict
) -> dict:
    sub = await _require_subscription(db, user_id)
    row = await _load_campaign(db, campaign_id=campaign_id, subscription_id=sub.id)

    if row.status not in {"draft", "scheduled", "paused"}:
        raise HTTPException(
            status_code=400,
            detail=f"Cannot edit a campaign in state '{row.status}'.",
        )

    if "name" in payload and payload["name"] is not None:
        row.name = str(payload["name"]).strip()
    if "message_body" in payload and payload["message_body"] is not None:
        row.message_body = str(payload["message_body"]).strip()
    if "media_url" in payload:
        row.media_url = payload["media_url"] or None
    if "scheduled_at" in payload:
        row.scheduled_at = payload["scheduled_at"]
        # keep status consistent with scheduling
        if row.status == "draft" and payload["scheduled_at"]:
            row.status = "scheduled"
        elif row.status == "scheduled" and not payload["scheduled_at"]:
            row.status = "draft"
    if "throttle_per_minute" in payload and payload["throttle_per_minute"] is not None:
        row.throttle_per_minute = int(payload["throttle_per_minute"])

    new_recipients = payload.get("recipients")
    if new_recipients is not None:
        cleaned = _dedupe_recipients(list(new_recipients))
        if len(cleaned) > MAX_RECIPIENTS_PER_CAMPAIGN:
            raise HTTPException(
                status_code=400,
                detail=f"Too many recipients (max {MAX_RECIPIENTS_PER_CAMPAIGN}).",
            )
        # wipe and reinsert — drafts are small and simpler than diffing.
        await db.execute(
            WhatsappCampaignRecipient.__table__.delete().where(
                WhatsappCampaignRecipient.campaign_id == row.id
            )
        )
        for r in cleaned:
            db.add(
                WhatsappCampaignRecipient(
                    campaign_id=row.id,
                    contact_id=r.get("contact_id"),
                    phone_number=r["phone_number"],
                    status="pending",
                )
            )
        row.total_recipients = len(cleaned)
        row.sent_count = 0
        row.delivered_count = 0
        row.replied_count = 0
        row.failed_count = 0

    await db.commit()
    await db.refresh(row)
    return _serialize(row)


async def delete_campaign(db: AsyncSession, user_id: str, campaign_id: int) -> None:
    sub = await _require_subscription(db, user_id)
    row = await _load_campaign(db, campaign_id=campaign_id, subscription_id=sub.id)
    if row.status == "running":
        raise HTTPException(
            status_code=400, detail="Pause the campaign before deleting it."
        )
    await db.execute(
        WhatsappCampaignRecipient.__table__.delete().where(
            WhatsappCampaignRecipient.campaign_id == row.id
        )
    )
    await db.delete(row)
    await db.commit()


async def transition_campaign(
    db: AsyncSession, user_id: str, campaign_id: int, action: str
) -> dict:
    """Apply a lifecycle action to a campaign.

    Supported actions: ``start``, ``pause``, ``resume``, ``cancel``.
    The actual upstream sending is handled by the dispatcher — this
    function only moves the state machine forward and records timestamps.
    """
    sub = await _require_subscription(db, user_id)
    row = await _load_campaign(db, campaign_id=campaign_id, subscription_id=sub.id)
    now = datetime.now(timezone.utc)

    action = (action or "").lower().strip()
    if action == "start":
        if row.status not in {"draft", "scheduled", "paused"}:
            raise HTTPException(400, f"Cannot start campaign in state '{row.status}'.")
        if row.total_recipients <= 0:
            raise HTTPException(400, "Add at least one recipient before starting.")
        row.status = "running"
        if row.started_at is None:
            row.started_at = now
    elif action == "pause":
        if row.status != "running":
            raise HTTPException(400, "Only running campaigns can be paused.")
        row.status = "paused"
    elif action == "resume":
        if row.status != "paused":
            raise HTTPException(400, "Only paused campaigns can be resumed.")
        row.status = "running"
    elif action == "cancel":
        if row.status in {"completed", "failed"}:
            raise HTTPException(400, f"Campaign already {row.status}.")
        row.status = "failed"
        row.completed_at = now
        row.last_error = "Cancelled by user."
    else:
        raise HTTPException(400, f"Unknown action '{action}'.")

    await db.commit()
    await db.refresh(row)
    return _serialize(row)


async def campaign_stats(db: AsyncSession, user_id: str) -> dict:
    """Aggregate stats across all campaigns for the dashboard header."""
    sub = await _require_subscription(db, user_id)
    stmt = select(
        func.count(WhatsappCampaign.id),
        func.coalesce(func.sum(WhatsappCampaign.total_recipients), 0),
        func.coalesce(func.sum(WhatsappCampaign.sent_count), 0),
        func.coalesce(func.sum(WhatsappCampaign.delivered_count), 0),
        func.coalesce(func.sum(WhatsappCampaign.replied_count), 0),
        func.coalesce(func.sum(WhatsappCampaign.failed_count), 0),
    ).where(WhatsappCampaign.subscription_id == sub.id)
    row = (await db.execute(stmt)).one()
    return {
        "campaigns": int(row[0] or 0),
        "recipients": int(row[1] or 0),
        "sent": int(row[2] or 0),
        "delivered": int(row[3] or 0),
        "replied": int(row[4] or 0),
        "failed": int(row[5] or 0),
    }