import hmac
from datetime import timedelta

from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, Field
from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError

from . import phone as phonelib
from . import templating
from .adapters.factory import get_adapter
from .config import settings
from .db import Contact, Message, SessionLocal, init_db, utcnow

app = FastAPI(title="WhatsApp Notification Gateway", version="1.0.0")
adapter = get_adapter()

STOP_WORDS = {"stop", "unsubscribe", "ايقاف", "إيقاف", "الغاء", "إلغاء", "وقف"}
START_WORDS = {"start", "subscribe", "تفعيل", "اشتراك", "ابدأ", "ابدا"}


@app.on_event("startup")
def _startup():
    init_db()


# ------------------------------------------------------------------ auth
def _match(candidate: str, valid) -> str | None:
    for key in valid:
        if key and hmac.compare_digest(candidate or "", key):
            return key
    return None


def client_auth(x_api_key: str = Header(default="")) -> str:
    key = _match(x_api_key, settings.api_keys.keys())
    if not key:
        raise HTTPException(401, "invalid X-API-Key")
    return settings.api_keys[key]


def admin_auth(key: str = Query(default=""), x_admin_key: str = Header(default="")) -> None:
    if not settings.admin_key or not _match(key or x_admin_key, [settings.admin_key]):
        raise HTTPException(401, "invalid admin key")


# ------------------------------------------------------------------ API for the LMS
class NotifyIn(BaseModel):
    event: str = Field(..., examples=["course_enrolled"])
    to: str = Field(..., description="phone in any format: +9665.., 05.., 009665..")
    locale: str | None = None
    vars: dict = Field(default_factory=dict)
    email: str | None = Field(None, description="used for e-mail fallback")
    text: str | None = Field(None, description="raw text; overrides the template")
    idempotency_key: str | None = None


@app.post("/notify", status_code=202)
def notify(body: NotifyIn, client: str = Depends(client_auth)):
    number = phonelib.normalize(body.to)
    if not number:
        raise HTTPException(422, f"invalid phone number: {body.to!r}")
    locale = body.locale or settings.default_locale
    text = (body.text or "").strip() or templating.render(body.event, locale, body.vars)
    if not text:
        raise HTTPException(422, f"unknown event {body.event!r}: add it to templates.yml or send 'text'")

    with SessionLocal() as db:
        if body.idempotency_key:
            old = db.scalar(select(Message).where(Message.idempotency_key == body.idempotency_key))
            if old:
                return {"id": old.id, "status": old.status, "duplicate": True}
        msg = Message(client=client, idempotency_key=body.idempotency_key, event=body.event, to_phone=number,
                      email=body.email, locale=locale, vars=body.vars, body=text)
        db.add(msg)
        try:
            db.commit()
        except IntegrityError:  # two identical requests at the same moment
            db.rollback()
            old = db.scalar(select(Message).where(Message.idempotency_key == body.idempotency_key))
            return {"id": old.id, "status": old.status, "duplicate": True}
        return {"id": msg.id, "status": msg.status, "duplicate": False}


@app.get("/messages/{msg_id}")
def get_message(msg_id: str, _: str = Depends(client_auth)):
    with SessionLocal() as db:
        msg = db.get(Message, msg_id)
        if not msg:
            raise HTTPException(404, "not found")
        return msg.as_dict()


@app.get("/messages")
def list_messages(status: str | None = None, to: str | None = None, limit: int = 50, _: str = Depends(client_auth)):
    with SessionLocal() as db:
        q = select(Message).order_by(Message.created_at.desc()).limit(min(limit, 500))
        if status:
            q = q.where(Message.status == status)
        if to:
            q = q.where(Message.to_phone == (phonelib.normalize(to) or to))
        return [m.as_dict() for m in db.scalars(q)]


@app.get("/health")
def health():
    state = adapter.connection_state()
    with SessionLocal() as db:
        since = utcnow() - timedelta(hours=24)
        rows = db.execute(select(Message.status, func.count()).where(Message.created_at >= since)
                          .group_by(Message.status)).all()
    return {"ok": state == "open", "provider": adapter.name, "whatsapp_state": state,
            "last_24h": {s: c for s, c in rows}, "daily_cap": settings.daily_cap}


# ------------------------------------------------------------------ webhook from Evolution
def _queue_system(db, number: str, event: str):
    text = templating.render(event, settings.default_locale, {})
    if text:
        db.add(Message(client="system", event=event, to_phone=number, locale=settings.default_locale, vars={}, body=text))


def _handle_inbound(db, item: dict):
    key = item.get("key") or {}
    if key.get("fromMe"):
        return
    jid = key.get("remoteJid") or ""
    if jid.endswith("@g.us") or "broadcast" in jid:
        return
    if jid.endswith("@lid"):  # privacy ids: real number is in an alternative field
        jid = key.get("senderPn") or key.get("remoteJidAlt") or item.get("senderPn") or ""
    number = phonelib.normalize(jid)
    if not number:
        return
    message = item.get("message") or {}
    text = (message.get("conversation") or (message.get("extendedTextMessage") or {}).get("text") or "").strip().lower()
    contact = db.get(Contact, number) or Contact(phone=number)
    contact.name = item.get("pushName") or contact.name
    if text in STOP_WORDS:
        if not contact.opted_out:
            _queue_system(db, number, "optout_confirmed")
        contact.opted_out, contact.opted_in = True, False
    elif contact.opted_out:
        if text in START_WORDS:
            contact.opted_out, contact.opted_in = False, True
            _queue_system(db, number, "optin_confirmed")
    elif not contact.opted_in:
        contact.opted_in = True
        _queue_system(db, number, "optin_confirmed")
    db.merge(contact)


_STATUS = {"DELIVERY_ACK": "delivered", "READ": "read", "PLAYED": "read", "delivered": "delivered", "read": "read"}
_RANK = {"sent": 1, "delivered": 2, "read": 3}


def _handle_status(db, item: dict):
    msg_id = item.get("keyId") or (item.get("key") or {}).get("id") or item.get("id")
    new = _STATUS.get(str(item.get("status") or ""))
    if not msg_id or not new:
        return
    msg = db.scalar(select(Message).where(Message.provider_msg_id == msg_id))
    if msg and _RANK.get(new, 0) > _RANK.get(msg.status, 0):
        msg.status = new


@app.post("/webhook/evolution")
async def evolution_webhook(request: Request, token: str = Query(default="")):
    if not settings.webhook_token or not hmac.compare_digest(token, settings.webhook_token):
        raise HTTPException(401, "bad token")
    try:
        payload = await request.json()
    except ValueError:
        return {"ok": True}
    event = str(payload.get("event", "")).lower().replace("_", ".")
    data = payload.get("data") or {}
    items = data if isinstance(data, list) else [data]
    with SessionLocal() as db:
        for item in items:
            if not isinstance(item, dict):
                continue
            if event == "messages.upsert":
                _handle_inbound(db, item)
            elif event == "messages.update":
                _handle_status(db, item)
            elif event == "connection.update":
                print(f"[whatsapp] connection state: {item.get('state')} {item.get('statusReason', '')}", flush=True)
        db.commit()
    return {"ok": True}


# ------------------------------------------------------------------ admin (setup + QR)
@app.get("/admin/qr", response_class=HTMLResponse)
def admin_qr(_: None = Depends(admin_auth)):
    if adapter.name != "evolution":
        return "<h3>PROVIDER is not evolution - no QR needed.</h3>"
    state = adapter.connection_state()
    log = ""
    if state == "no_instance":
        log = f"create: {adapter.create_instance()['status']}"
        state = adapter.connection_state()
    hook = f"{settings.gateway_internal_url}/webhook/evolution?token={settings.webhook_token}"
    log += f" | webhook: {adapter.set_webhook(hook)['status']}"
    if state == "open":
        body = "<h2 style='color:#0a7d33'>✅ WhatsApp connected</h2><p>The gateway is ready to send.</p>"
    else:
        data = adapter.connect()
        img = data.get("base64") or ""
        if img and not img.startswith("data:"):
            img = "data:image/png;base64," + img
        pairing = data.get("pairingCode") or ""
        body = (f"<h2>Scan with WhatsApp → Linked devices</h2><img src='{img}' width='320' alt='QR'>"
                if img else f"<h2>No QR yet</h2><pre>{str(data)[:600]}</pre>")
        if pairing:
            body += f"<p>Pairing code: <b>{pairing}</b></p>"
        body += "<p>Page refreshes every 20s.</p><meta http-equiv='refresh' content='20'>"
    return (f"<html><body style='font-family:sans-serif;text-align:center;padding:40px'>{body}"
            f"<p style='color:#777'>state: {state} | {log}</p></body></html>")


@app.post("/admin/logout")
def admin_logout(_: None = Depends(admin_auth)):
    return adapter.logout() if adapter.name == "evolution" else {"detail": "n/a"}


@app.get("/admin/contacts")
def admin_contacts(_: None = Depends(admin_auth), limit: int = 200):
    with SessionLocal() as db:
        rows = db.scalars(select(Contact).order_by(Contact.updated_at.desc()).limit(limit))
        return [{"phone": c.phone, "name": c.name, "opted_in": c.opted_in, "opted_out": c.opted_out} for c in rows]
