"""Single sender loop: one message at a time, random human-like pauses, daily cap, retries, e-mail fallback."""
import random
import time
from datetime import timedelta

from sqlalchemy import func, select, update

from . import mailer
from .adapters.base import SendError
from .adapters.factory import get_adapter
from .config import settings
from .db import Contact, Message, SessionLocal, init_db, utcnow

adapter = get_adapter()


def log(*parts):
    print("[worker]", *parts, flush=True)


def sent_today(db) -> int:
    start = utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
    return db.scalar(select(func.count()).select_from(Message).where(
        Message.channel == "whatsapp", Message.sent_at >= start)) or 0


def claim_next(db, system_only: bool = False) -> Message | None:
    q = (select(Message).where(Message.status == "queued", Message.next_attempt_at <= utcnow())
         .order_by(Message.created_at).limit(1).with_for_update(skip_locked=True))
    if system_only:
        q = q.where(Message.client == "system")
    msg = db.scalar(q)
    if msg:
        msg.status = "sending"
        db.commit()
    return msg


def finish(db, msg: Message, status: str, channel: str | None = None, error: str | None = None, provider_id=None):
    msg.status, msg.error = status, error
    if channel:
        msg.channel = channel
    if provider_id:
        msg.provider_msg_id = provider_id
    if status == "sent":
        msg.sent_at = utcnow()
    db.commit()
    log(msg.id, msg.event, msg.to_phone, "->", status, channel or "", error or "")


def email_or(db, msg: Message, status: str, reason: str):
    """Try e-mail fallback; otherwise close the message with `status`."""
    if mailer.enabled() and msg.email:
        try:
            mailer.send(msg.email, f"{settings.site_name}", msg.body)
            return finish(db, msg, "sent", channel="email", error=f"fallback: {reason}")
        except Exception as exc:  # noqa: BLE001
            reason = f"{reason}; email failed: {exc}"
    finish(db, msg, status, error=reason)


def requeue(db, msg: Message, reason: str, delay_seconds: int, count_attempt: bool = True):
    if count_attempt:
        msg.attempts += 1
    if msg.attempts >= settings.max_attempts:
        return email_or(db, msg, "failed", f"max attempts reached: {reason}")
    msg.status, msg.error = "queued", reason
    msg.next_attempt_at = utcnow() + timedelta(seconds=delay_seconds)
    db.commit()


def too_old(msg: Message) -> bool:
    created = msg.created_at if msg.created_at.tzinfo else msg.created_at.replace(tzinfo=utcnow().tzinfo)
    return utcnow() - created > timedelta(minutes=settings.fallback_after_minutes)


def process(db, msg: Message) -> bool:
    """Returns True when a WhatsApp message was really sent (=> apply the long pause)."""
    contact = db.get(Contact, msg.to_phone)
    is_system = msg.client == "system"
    if contact and contact.opted_out and msg.event != "optout_confirmed":
        finish(db, msg, "skipped", error="recipient opted out")
        return False
    if settings.require_opt_in and not is_system and not (contact and contact.opted_in):
        email_or(db, msg, "skipped", "recipient has not opted in on WhatsApp")
        return False

    state = adapter.connection_state()
    if state != "open":
        if too_old(msg):
            email_or(db, msg, "failed", f"whatsapp not connected ({state})")
        else:
            requeue(db, msg, f"whatsapp not connected ({state})", 60, count_attempt=False)
        return False

    if not is_system and sent_today(db) >= settings.daily_cap:
        if too_old(msg) and mailer.enabled() and msg.email:
            email_or(db, msg, "failed", "daily cap reached")
        else:
            requeue(db, msg, "daily cap reached", 900, count_attempt=False)
        return False

    if settings.check_number_exists and adapter.number_exists(msg.to_phone) is False:
        email_or(db, msg, "failed", "number is not on WhatsApp")
        return False

    try:
        provider_id = adapter.send(msg)
    except SendError as exc:
        if exc.retryable:
            requeue(db, msg, str(exc), 30 * (2 ** msg.attempts))
        else:
            email_or(db, msg, "failed", str(exc))
        return False
    finish(db, msg, "sent", channel="whatsapp", provider_id=provider_id)
    return True


def main():
    init_db()
    with SessionLocal() as db:  # recover messages stuck by a crash/restart
        db.execute(update(Message).where(Message.status == "sending").values(status="queued"))
        db.commit()
    log(f"started | provider={adapter.name} cap={settings.daily_cap}/day "
        f"delay={settings.min_delay}-{settings.max_delay}s opt_in_required={settings.require_opt_in}")
    while True:
        try:
            with SessionLocal() as db:
                msg = claim_next(db)
                sent = process(db, msg) if msg else False
            if sent:
                time.sleep(random.uniform(settings.min_delay, max(settings.min_delay, settings.max_delay)))
            else:
                time.sleep(2 if msg else 3)
        except Exception as exc:  # noqa: BLE001 - never die
            log("loop error:", repr(exc))
            time.sleep(10)


if __name__ == "__main__":
    main()
