# @File: backend/services/whatsapp/contacts.py
# @Desc: Contact book for the WhatsApp Reseller module — CRUD, CSV import,
#        opt-out flagging, tag filtering, and pagination. Works directly on
#        ``whatsapp_contacts`` with per-subscription isolation.
from __future__ import annotations

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

from fastapi import HTTPException, status
from models.whatsapp import WhatsappClientSubscription, WhatsappContact, WhatsappPackage
from sqlalchemy import and_, desc, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession

logger = logging.getLogger(__name__)

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


def _normalize_phone(phone: str) -> str:
    if not phone:
        return ""
    return re.sub(r"[\s\-()]", "", phone.strip())


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


def _normalize_tags(tags: Optional[str]) -> Optional[str]:
    """Normalise a comma-separated tag string: dedupe, lower-case, max 10 tags."""
    if not tags:
        return None
    raw = [t.strip().lower() for t in tags.split(",")]
    cleaned: list[str] = []
    for t in raw:
        if not t:
            continue
        if t in cleaned:
            continue
        if len(t) > 32:
            t = t[:32]
        cleaned.append(t)
        if len(cleaned) >= 10:
            break
    return ",".join(cleaned) if cleaned else None


async def _require_active_subscription(
    db: AsyncSession, user_id: str
) -> WhatsappClientSubscription:
    stmt = (
        select(WhatsappClientSubscription)
        .where(
            and_(
                WhatsappClientSubscription.user_id == user_id,
                WhatsappClientSubscription.status == "active",
            )
        )
        .order_by(desc(WhatsappClientSubscription.activated_at))
        .limit(1)
    )
    row = (await db.execute(stmt)).scalar_one_or_none()
    if not row:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="No active WhatsApp subscription.",
        )
    return row


def _serialize(c: WhatsappContact) -> dict:
    return {
        "id": c.id,
        "phone_number": c.phone_number,
        "display_name": c.display_name,
        "tags": [t for t in (c.tags or "").split(",") if t],
        "notes": c.notes,
        "is_opted_out": c.is_opted_out,
        "last_message_at": c.last_message_at.isoformat() if c.last_message_at else None,
        "created_at": c.created_at.isoformat() if c.created_at else None,
    }


async def _count_contacts(db: AsyncSession, subscription_id: int) -> int:
    stmt = select(func.count(WhatsappContact.id)).where(
        WhatsappContact.subscription_id == subscription_id
    )
    return int((await db.execute(stmt)).scalar() or 0)


async def _enforce_contact_quota(
    db: AsyncSession, sub: WhatsappClientSubscription, additions: int = 1
) -> None:
    pkg = (
        await db.execute(select(WhatsappPackage).where(WhatsappPackage.id == sub.package_id))
    ).scalar_one_or_none()
    if not pkg or pkg.contacts_unlimited:
        return
    current = await _count_contacts(db, sub.id)
    if current + additions > max(pkg.max_contacts, 0):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=(
                f"Contact limit reached ({pkg.max_contacts}). Upgrade your plan "
                "or delete existing contacts to add more."
            ),
        )


async def list_contacts(
    db: AsyncSession,
    user_id: str,
    search: Optional[str] = None,
    tag: Optional[str] = None,
    only_opted_in: bool = False,
    limit: int = 100,
    offset: int = 0,
) -> dict:
    sub = await _require_active_subscription(db, user_id)
    limit = max(1, min(limit, MAX_PAGE_SIZE))
    offset = max(0, offset)

    conditions = [WhatsappContact.subscription_id == sub.id]
    if search:
        like = f"%{search.strip()}%"
        conditions.append(
            or_(
                WhatsappContact.phone_number.ilike(like),
                WhatsappContact.display_name.ilike(like),
            )
        )
    if tag:
        conditions.append(WhatsappContact.tags.ilike(f"%{tag.strip().lower()}%"))
    if only_opted_in:
        conditions.append(WhatsappContact.is_opted_out.is_(False))

    total_stmt = select(func.count(WhatsappContact.id)).where(and_(*conditions))
    total = int((await db.execute(total_stmt)).scalar() or 0)

    stmt = (
        select(WhatsappContact)
        .where(and_(*conditions))
        .order_by(desc(WhatsappContact.created_at))
        .offset(offset)
        .limit(limit)
    )
    rows = (await db.execute(stmt)).scalars().all()
    return {
        "total": total,
        "limit": limit,
        "offset": offset,
        "items": [_serialize(c) for c in rows],
    }


async def create_contact(
    db: AsyncSession,
    user_id: str,
    phone_number: str,
    display_name: Optional[str] = None,
    tags: Optional[str] = None,
    notes: Optional[str] = None,
) -> dict:
    sub = await _require_active_subscription(db, user_id)
    phone = _normalize_phone(phone_number)
    if not _is_valid_phone(phone):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid phone number. Include country code, e.g. +15551234567.",
        )

    # De-dupe per subscription.
    existing = (
        await db.execute(
            select(WhatsappContact).where(
                and_(
                    WhatsappContact.subscription_id == sub.id,
                    WhatsappContact.phone_number == phone,
                )
            )
        )
    ).scalar_one_or_none()
    if existing:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="A contact with this phone number already exists.",
        )

    await _enforce_contact_quota(db, sub, additions=1)

    contact = WhatsappContact(
        subscription_id=sub.id,
        user_id=user_id,
        phone_number=phone,
        display_name=(display_name or "").strip()[:128] or None,
        tags=_normalize_tags(tags),
        notes=(notes or "").strip()[:2000] or None,
    )
    db.add(contact)
    await db.flush()
    sub.contacts_used = await _count_contacts(db, sub.id)
    await db.commit()
    await db.refresh(contact)
    return _serialize(contact)


async def update_contact(
    db: AsyncSession,
    user_id: str,
    contact_id: int,
    display_name: Optional[str] = None,
    tags: Optional[str] = None,
    notes: Optional[str] = None,
    is_opted_out: Optional[bool] = None,
) -> dict:
    sub = await _require_active_subscription(db, user_id)
    contact = (
        await db.execute(
            select(WhatsappContact).where(
                and_(
                    WhatsappContact.id == contact_id,
                    WhatsappContact.subscription_id == sub.id,
                )
            )
        )
    ).scalar_one_or_none()
    if not contact:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found."
        )
    if display_name is not None:
        contact.display_name = display_name.strip()[:128] or None
    if tags is not None:
        contact.tags = _normalize_tags(tags)
    if notes is not None:
        contact.notes = notes.strip()[:2000] or None
    if is_opted_out is not None:
        contact.is_opted_out = bool(is_opted_out)
    await db.commit()
    await db.refresh(contact)
    return _serialize(contact)


async def delete_contact(db: AsyncSession, user_id: str, contact_id: int) -> None:
    sub = await _require_active_subscription(db, user_id)
    contact = (
        await db.execute(
            select(WhatsappContact).where(
                and_(
                    WhatsappContact.id == contact_id,
                    WhatsappContact.subscription_id == sub.id,
                )
            )
        )
    ).scalar_one_or_none()
    if not contact:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found."
        )
    await db.delete(contact)
    await db.commit()
    sub.contacts_used = await _count_contacts(db, sub.id)
    await db.commit()


async def bulk_import_csv(
    db: AsyncSession, user_id: str, csv_text: str
) -> dict:
    """Import contacts from CSV text.

    Expected columns (header row required, case-insensitive, extra cols ok):
        phone, name (optional), tags (optional, comma-separated), notes (optional)
    Returns a summary: ``{inserted, skipped_existing, skipped_invalid, errors[]}``.
    """
    sub = await _require_active_subscription(db, user_id)
    if not csv_text or not csv_text.strip():
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, detail="CSV content is empty."
        )

    reader = csv.DictReader(io.StringIO(csv_text))
    if not reader.fieldnames:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="CSV must include a header row (phone, name, tags, notes).",
        )
    # Case-insensitive header map.
    headers_lower = {h.lower(): h for h in reader.fieldnames}
    if "phone" not in headers_lower:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="CSV must contain a 'phone' column.",
        )

    # Preload existing phones for fast de-dupe.
    existing_rows = (
        await db.execute(
            select(WhatsappContact.phone_number).where(
                WhatsappContact.subscription_id == sub.id
            )
        )
    ).scalars().all()
    existing = set(existing_rows)

    inserted = 0
    skipped_existing = 0
    skipped_invalid = 0
    errors: list[str] = []

    # Quota check upfront (row count including invalid ones is over-estimate,
    # but it's a safer gate).
    pkg = (
        await db.execute(select(WhatsappPackage).where(WhatsappPackage.id == sub.package_id))
    ).scalar_one_or_none()
    unlimited = bool(pkg and pkg.contacts_unlimited)

    new_contacts: list[WhatsappContact] = []
    for idx, raw in enumerate(reader, start=2):  # start=2 because header is row 1
        if idx > 10_000:
            errors.append("Truncated at 10,000 rows.")
            break
        phone_raw = (raw.get(headers_lower["phone"]) or "").strip()
        phone = _normalize_phone(phone_raw)
        if not _is_valid_phone(phone):
            skipped_invalid += 1
            if len(errors) < 20:
                errors.append(f"Row {idx}: invalid phone '{phone_raw}'")
            continue
        if phone in existing:
            skipped_existing += 1
            continue

        if not unlimited and pkg and (await _count_contacts(db, sub.id)) + len(new_contacts) + 1 > pkg.max_contacts:
            errors.append(
                f"Plan limit of {pkg.max_contacts} contacts reached; stopping at row {idx}."
            )
            break

        name = (raw.get(headers_lower.get("name", "")) or "").strip() if "name" in headers_lower else ""
        tags = (raw.get(headers_lower.get("tags", "")) or "").strip() if "tags" in headers_lower else ""
        notes = (raw.get(headers_lower.get("notes", "")) or "").strip() if "notes" in headers_lower else ""

        new_contacts.append(
            WhatsappContact(
                subscription_id=sub.id,
                user_id=user_id,
                phone_number=phone,
                display_name=name[:128] or None,
                tags=_normalize_tags(tags),
                notes=notes[:2000] or None,
            )
        )
        existing.add(phone)
        inserted += 1

    if new_contacts:
        db.add_all(new_contacts)
        await db.flush()
    sub.contacts_used = await _count_contacts(db, sub.id)
    await db.commit()

    return {
        "inserted": inserted,
        "skipped_existing": skipped_existing,
        "skipped_invalid": skipped_invalid,
        "errors": errors,
    }


async def list_tags(db: AsyncSession, user_id: str) -> list[dict]:
    """Return distinct tags with counts, useful for campaign targeting."""
    sub = await _require_active_subscription(db, user_id)
    rows = (
        await db.execute(
            select(WhatsappContact.tags).where(WhatsappContact.subscription_id == sub.id)
        )
    ).scalars().all()
    counts: dict[str, int] = {}
    for tags_str in rows:
        if not tags_str:
            continue
        for t in tags_str.split(","):
            t = t.strip()
            if not t:
                continue
            counts[t] = counts.get(t, 0) + 1
    return [
        {"tag": t, "count": counts[t]}
        for t in sorted(counts.keys(), key=lambda x: (-counts[x], x))
    ]