#!/usr/bin/env python3
"""
Receiver de webhooks GHL para PX3 Lab.

Fluxo:
  Workflow GHL (Task Created / Completed / Updated)
    -> POST https://receiver-ghl-px3.agentesclimb.us/webhook/task
    -> extrai contactId
    -> busca oportunidades do contato
    -> recalcula farol (mesma lógica do farois_pipeline)
    -> faz PUT no custom field Status Tarefa

Endpoint auxiliar:
  GET /health -> {"ok": true}

Rate limit: 100 req/min (janela deslizante em memória).
Log: /opt/mia/logs/receiver_ghl_px3.log (rotacionado 10MB x 3).
"""

from __future__ import annotations

import hashlib
import hmac
import json
import logging
import os
import sys
import threading
import time
from collections import deque
from datetime import datetime, timezone
from logging.handlers import RotatingFileHandler
from pathlib import Path

import requests
from flask import Flask, jsonify, request
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------

ENV_FILE = "/opt/mia/config/farois_px3.env"
LOG_PATH = "/opt/mia/logs/receiver_ghl_px3.log"

GHL_BASE = "https://services.leadconnectorhq.com"
CAMPO_STATUS_ID = "cvrJJ7rlAuIGgCL42NEH"

RATE_LIMIT_MAX = 100
RATE_LIMIT_WINDOW = 60  # segundos


def _load_env(path: str) -> None:
    if not os.path.isfile(path):
        return
    with open(path, "r", encoding="utf-8") as f:
        for raw in f:
            line = raw.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            key, _, val = line.partition("=")
            os.environ.setdefault(key.strip(), val.strip().strip('"').strip("'"))


_load_env(ENV_FILE)

GHL_TOKEN = os.getenv("GHL_TOKEN", "").strip()
GHL_LOCATION = os.getenv("GHL_LOCATION", "").strip()
GHL_WEBHOOK_SECRET = os.getenv("GHL_WEBHOOK_SECRET", "").strip()  # opcional

if not GHL_TOKEN or not GHL_LOCATION:
    sys.stderr.write(f"[FATAL] Faltam GHL_TOKEN/GHL_LOCATION no {ENV_FILE}\n")
    sys.exit(2)

HEADERS = {
    "Authorization": f"Bearer {GHL_TOKEN}",
    "Version": "2021-07-28",
    "Content-Type": "application/json",
}

# ---------------------------------------------------------------------------
# Logger
# ---------------------------------------------------------------------------

Path(LOG_PATH).parent.mkdir(parents=True, exist_ok=True)

logger = logging.getLogger("receiver_ghl_px3")
logger.setLevel(logging.INFO)
logger.propagate = False
if not logger.handlers:
    handler = RotatingFileHandler(
        LOG_PATH, maxBytes=10 * 1024 * 1024, backupCount=3, encoding="utf-8"
    )
    handler.setFormatter(
        logging.Formatter("%(asctime)s %(levelname)s %(message)s", "%Y-%m-%d %H:%M:%S")
    )
    logger.addHandler(handler)
    stream = logging.StreamHandler(sys.stdout)
    stream.setFormatter(
        logging.Formatter("%(asctime)s %(levelname)s %(message)s", "%Y-%m-%d %H:%M:%S")
    )
    logger.addHandler(stream)

log = logger.info


# ---------------------------------------------------------------------------
# Session HTTP
# ---------------------------------------------------------------------------

def _build_session() -> requests.Session:
    s = requests.Session()
    retry = Retry(
        total=3,
        backoff_factor=1.0,
        status_forcelist=(429, 500, 502, 503, 504),
        allowed_methods=frozenset(["GET", "PUT", "POST"]),
        respect_retry_after_header=True,
        raise_on_status=False,
    )
    adapter = HTTPAdapter(max_retries=retry, pool_connections=16, pool_maxsize=16)
    s.mount("https://", adapter)
    s.mount("http://", adapter)
    s.headers.update(HEADERS)
    return s


SESSION = _build_session()


# ---------------------------------------------------------------------------
# Rate limit simples
# ---------------------------------------------------------------------------

_rate_hits: deque[float] = deque()
_rate_lock = threading.Lock()


def _within_rate_limit() -> bool:
    now = time.monotonic()
    with _rate_lock:
        while _rate_hits and now - _rate_hits[0] > RATE_LIMIT_WINDOW:
            _rate_hits.popleft()
        if len(_rate_hits) >= RATE_LIMIT_MAX:
            return False
        _rate_hits.append(now)
        return True


# ---------------------------------------------------------------------------
# Validação de assinatura (opcional)
# ---------------------------------------------------------------------------

def _valid_signature(body_bytes: bytes) -> bool:
    """
    Se GHL_WEBHOOK_SECRET estiver setado, exige header X-Wh-Signature
    (HMAC-SHA256 hex). Se não estiver, aceita tudo (comportamento default).
    """
    if not GHL_WEBHOOK_SECRET:
        return True
    provided = (
        request.headers.get("X-Wh-Signature")
        or request.headers.get("X-Ghl-Signature")
        or ""
    ).strip()
    if not provided:
        return False
    expected = hmac.new(
        GHL_WEBHOOK_SECRET.encode("utf-8"), body_bytes, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(provided, expected)


# ---------------------------------------------------------------------------
# GHL helpers (mesma lógica do farois_pipeline)
# ---------------------------------------------------------------------------

def buscar_tarefas_contato(contact_id: str) -> tuple[str, list[dict]]:
    try:
        r = SESSION.get(f"{GHL_BASE}/contacts/{contact_id}/tasks", timeout=15)
    except requests.RequestException as e:
        log(f"[{contact_id}] falha ao ler tarefas: {e!r}")
        return ("fail", [])
    if r.status_code == 404:
        return ("empty", [])
    if r.status_code != 200:
        log(f"[{contact_id}] tarefas HTTP {r.status_code} - {r.text[:200]}")
        return ("fail", [])
    try:
        tarefas = r.json().get("tasks") or []
    except ValueError:
        return ("fail", [])
    return ("ok" if tarefas else "empty", tarefas)


def buscar_oportunidades_contato(contact_id: str) -> list[dict]:
    """Oportunidades abertas de um contato."""
    params = {
        "location_id": GHL_LOCATION,
        "contact_id": contact_id,
        "status": "open",
        "limit": 100,
    }
    try:
        r = SESSION.get(f"{GHL_BASE}/opportunities/search", params=params, timeout=15)
    except requests.RequestException as e:
        log(f"[{contact_id}] falha ao buscar opps: {e!r}")
        return []
    if r.status_code != 200:
        log(f"[{contact_id}] opps HTTP {r.status_code} - {r.text[:200]}")
        return []
    try:
        return r.json().get("opportunities") or []
    except ValueError:
        return []


def calcular_farol(tarefas: list[dict]) -> str:
    pendentes = [t for t in tarefas if not t.get("completed")]
    if not pendentes:
        return "🟡"
    agora = datetime.now(timezone.utc)
    for t in pendentes:
        due = t.get("dueDate")
        if not due:
            continue
        try:
            if isinstance(due, (int, float)):
                dt = datetime.fromtimestamp(due / 1000, tz=timezone.utc)
            else:
                dt = datetime.fromisoformat(str(due).replace("Z", "+00:00"))
            if dt < agora:
                return "🔴"
        except Exception:
            continue
    return "🟢"


def atualizar_opp(opp_id: str, farol: str) -> bool:
    body = {"customFields": [{"id": CAMPO_STATUS_ID, "value": farol}]}
    try:
        r = SESSION.put(f"{GHL_BASE}/opportunities/{opp_id}", json=body, timeout=15)
    except requests.RequestException as e:
        log(f"[{opp_id}] falha ao atualizar: {e!r}")
        return False
    if r.status_code in (200, 201):
        return True
    log(f"[{opp_id}] update HTTP {r.status_code} - {r.text[:200]}")
    return False


def _extract_contact_id(payload: dict) -> str | None:
    """Aceita vários formatos usados pelo GHL Workflow / Webhook Task."""
    if not isinstance(payload, dict):
        return None
    # Formatos comuns:
    #   {"contactId": "..."}
    #   {"contact_id": "..."}
    #   {"contact": {"id": "..."}}
    #   {"task": {"contactId": "..."}} ou {"data": {"contactId": "..."}}
    #   {"customData": {"contact_id": "..."}}
    candidatos = [
        payload.get("contactId"),
        payload.get("contact_id"),
        (payload.get("contact") or {}).get("id"),
        (payload.get("task") or {}).get("contactId"),
        (payload.get("task") or {}).get("contact_id"),
        (payload.get("data") or {}).get("contactId"),
        (payload.get("data") or {}).get("contact_id"),
        (payload.get("customData") or {}).get("contactId"),
        (payload.get("customData") or {}).get("contact_id"),
    ]
    for c in candidatos:
        if isinstance(c, str) and c.strip():
            return c.strip()
    return None


# ---------------------------------------------------------------------------
# Flask app
# ---------------------------------------------------------------------------

app = Flask(__name__)


@app.get("/health")
def health():
    return jsonify(ok=True, service="receiver-ghl-px3", ts=time.time())


@app.post("/webhook/task")
def webhook_task():
    if not _within_rate_limit():
        return jsonify(ok=False, error="rate_limited"), 429

    raw = request.get_data(cache=True)
    if not _valid_signature(raw):
        log(f"assinatura inválida (headers={dict(request.headers)!r})")
        return jsonify(ok=False, error="invalid_signature"), 401

    try:
        payload = json.loads(raw.decode("utf-8")) if raw else {}
    except ValueError:
        return jsonify(ok=False, error="invalid_json"), 400

    contact_id = _extract_contact_id(payload)
    if not contact_id:
        log(f"payload sem contactId: {json.dumps(payload)[:400]}")
        return jsonify(ok=False, error="missing_contact_id"), 400

    log(f"[{contact_id}] webhook recebido")

    status, tarefas = buscar_tarefas_contato(contact_id)
    if status == "fail":
        return jsonify(ok=False, error="fail_read_tasks"), 502

    farol = calcular_farol(tarefas)
    opps = buscar_oportunidades_contato(contact_id)
    if not opps:
        log(f"[{contact_id}] nenhuma opp aberta; farol calculado={farol}")
        return jsonify(ok=True, contactId=contact_id, farol=farol, opps=0)

    atualizados, falhas = 0, 0
    for opp in opps:
        opp_id = opp.get("id")
        if not opp_id:
            continue
        if atualizar_opp(opp_id, farol):
            atualizados += 1
        else:
            falhas += 1

    log(f"[{contact_id}] farol={farol} opps={len(opps)} atualizados={atualizados} falhas={falhas}")
    return jsonify(
        ok=True,
        contactId=contact_id,
        farol=farol,
        opps=len(opps),
        atualizados=atualizados,
        falhas=falhas,
    )


if __name__ == "__main__":
    port = int(os.getenv("PORT", "8945"))
    host = os.getenv("HOST", "0.0.0.0")
    log(f"receiver-ghl-px3 subindo em {host}:{port}")
    app.run(host=host, port=port)
