import os
import time
import asyncio
from datetime import datetime
import json
import requests
import base64
from io import BytesIO
from urllib.parse import urlparse

from telethon import TelegramClient
from telethon.tl.functions.messages import ImportChatInviteRequest
from telethon.utils import get_display_name
from telethon.errors import FloodWaitError

# --- extras para filtro/normalización ---
import re
import unicodedata

# Carpeta base (para guardar state_*.json y la sesión de Telethon)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# ---------------- Configuración (ENV + fallback) ----------------
api_id = int(os.getenv("TG_API_ID", "22867179"))
api_hash = os.getenv("TG_API_HASH", "d6ed2db982869e73b34f0a70d3257ade")
phone_number = os.getenv("TG_PHONE", "+18294198479")
webhook_url = os.getenv("WEBHOOK_URL", "https://hook.us1.make.com/sdq81hn8e8h41ww6zbr9hm4mbht7maux")

# Bot para obtener file_id (necesario para Make -> Download a File)
BOT_TOKEN = os.getenv("TG_BOT_TOKEN", "6618856215:AAEKWBjrs_qs0zb85njLUw3j_KSlxJO8Els")
BOT_CHAT_ID = os.getenv("TG_BOT_CHAT_ID", "514400045")  # tu chat privado con el bot

# Tamaño máx para fallback base64 (~5MB)
MAX_INLINE_BYTES = 5 * 1024 * 1024

# ¿Permitir imágenes sin caption si vienen solas?
ALLOW_MEDIA_ONLY = True

# Resolver por @/link (recomendado)
FORCE_USERNAME_OR_LINK = os.getenv("TARGET_LINK_OR_AT", "https://t.me/+cRjmc1Iq-xwzYjE5")
# Fallback por nombre si el link fallara (opcional)
TARGET_QUERY = os.getenv("TARGET_QUERY", "THE GOLD BROTHERS")

# Intervalo de escaneo
POLL_SECONDS = 10

# Reintentos HTTP
HTTP_MAX_RETRIES = 3
HTTP_BACKOFF_BASE = 1.5  # segundos

# ------------------------------------------------


# --------------- Filtro de “señales” ---------------
def _normalize(s: str) -> str:
    if not s:
        return ""
    s = unicodedata.normalize("NFD", s)
    s = "".join(ch for ch in s if unicodedata.category(ch) != "Mn")  # quita tildes
    return s.lower().strip()

SYMBOL_RE = re.compile(r"\b(xauusd|xagusd|eurusd|gbpusd|nas100|us30|spx|sp500|dow|dj30|gold|oro|btc(usdt)?)\b")
ACTION_RE = re.compile(r"\b(buy|sell|compr(ar|a)|vender|long|short)\b")
SL_RE     = re.compile(r"\b(sl|stop\s*loss)\b")
TP_RE     = re.compile(r"\btp\s*\d*\b")
PIPS_RE   = re.compile(r"\b\+?\d{1,4}\s*pips?\b")
RR_RE     = re.compile(r"\brr\b[: ]?\s*\d+(\.\d+)?\s*[:/]\s*\d+(\.\d+)?")
PRICE_RE  = re.compile(r"@\s*\d{3,5}(\.\d+)?(?:\s*-\s*\d{3,5}(\.\d+)?)?")

# Ampliado para detectar cabeceras de resumen diario en ES/EN
DAILY_RE  = re.compile(r"(rendimiento\s+diario|daily\s+performance|ganancia/pera?dida|tasa\s+de\s+exito|porcentaje\s+de\s+acierto)")
CHITCHAT_RE = re.compile(
    r"(manden(me)?\s+sus\s+resultados|envi[ea]n?\s+sus\s+resultados|"
    r"hablar\s+conmigo|hay\s+que\s+hablar|dios\s*mio)"
)

def is_signal(text: str, has_media: bool) -> bool:
    """
    Lógica:
      - Si es RESUMEN DIARIO y contiene 'pips' o métricas -> True.
      - Si NO es resumen: necesita score >= 3 Y al menos un 'núcleo de señal':
            {símbolo | acción | pips | precio | RR}.
      - SL/TP solos no alcanzan.
      - Imagen sin caption: True si ALLOW_MEDIA_ONLY.
    """
    t = _normalize(text)

    if CHITCHAT_RE.search(t):
        return False

    # Caso 1: Resumen diario (aceptamos sin núcleo, pero con evidencia)
    if DAILY_RE.search(t) and (PIPS_RE.search(t) or "pips" in t or "ganancia" in t or "perdida" in t
                               or "tasa de exito" in t or "porcentaje de acierto" in t):
        return True

    score = 0
    if SYMBOL_RE.search(t): score += 2
    if ACTION_RE.search(t): score += 2
    if SL_RE.search(t):     score += 2
    if TP_RE.search(t):     score += 2
    if PIPS_RE.search(t):   score += 1
    if RR_RE.search(t):     score += 1
    if PRICE_RE.search(t):  score += 1

    # Núcleo de señal: al menos uno de estos
    has_core = any([
        SYMBOL_RE.search(t),
        ACTION_RE.search(t),
        PIPS_RE.search(t),
        PRICE_RE.search(t),
        RR_RE.search(t)
    ])

    if score >= 3 and has_core:
        return True

    # Imagen sin caption: permitido si ALLOW_MEDIA_ONLY=True
    if has_media and not t and ALLOW_MEDIA_ONLY:
        return True

    return False
# ----------------------------------------------------


# ----------------- Utilidades estado -----------------
def state_path_for_target(target) -> str:
    tid = getattr(target, "id", "unknown")
    return os.path.join(BASE_DIR, f"state_{tid}.json")

def load_last_id(path: str) -> int:
    try:
        with open(path, "r", encoding="utf-8") as f:
            return int(json.load(f).get("last_id", 0))
    except Exception:
        return 0

def save_last_id(path: str, last_id: int) -> None:
    try:
        with open(path, "w", encoding="utf-8") as f:
            json.dump({"last_id": last_id}, f)
    except Exception as e:
        print(f"[{datetime.now()}] ⚠️ No pude guardar estado: {e}")
# ----------------------------------------------------


# ----------------- Resolver por link/@ -----------------
def _norm_title(s: str) -> str:
    s = unicodedata.normalize("NFKD", s or "")
    s = "".join(ch for ch in s if unicodedata.category(ch) != "Mn")
    s = re.sub(r"[^\w\s]", " ", s, flags=re.UNICODE)
    s = re.sub(r"\s+", " ", s).strip().lower()
    return s

def _invite_hash_from_url(url: str):
    try:
        u = urlparse(url)
        if u.netloc not in ("t.me", "telegram.me"):
            return None
        path = u.path.lstrip("/")
        if path.startswith("+"):
            return path[1:]
        if path.startswith("joinchat/"):
            return path.split("/", 1)[1]
    except Exception:
        pass
    return None

async def resolve_target(client):
    # 1) Por @usuario o link de invitación
    if FORCE_USERNAME_OR_LINK:
        try:
            ent = await client.get_entity(FORCE_USERNAME_OR_LINK)
            return ent  # ENTIDAD directa
        except ValueError:
            inv = _invite_hash_from_url(FORCE_USERNAME_OR_LINK)
            if inv:
                try:
                    await client(ImportChatInviteRequest(inv))
                    ent = await client.get_entity(FORCE_USERNAME_OR_LINK)
                    return ent
                except Exception as e:
                    print(f"⚠️ No pude unirme con el hash del link: {e}")
        except Exception as e:
            print(f"⚠️ No pude resolver por link/@: {e}")

    # 2) Fallback por nombre (a prueba de emojis)
    q = _norm_title(TARGET_QUERY)
    dialogs = await client.get_dialogs()
    candidates = [d for d in dialogs if d.name and q in _norm_title(d.name)]
    if not candidates:
        return None

    # Preferir canal broadcast (suscriptores) frente a megagroup
    broadcast = [d for d in candidates if getattr(d.entity, "megagroup", None) is False]
    chosen = broadcast[0] if broadcast else candidates[0]
    return chosen.entity  # ENTIDAD
# -------------------------------------------------------


# ----------------- Sanitizador de texto (encabezados) -----------------
HEADER_DAILY_RE = re.compile(r".*\bdaily\s+performance\b.*", re.I)

def sanitize_text(text: str, channel_title: str) -> str:
    """
    - Si la primera línea contiene 'Daily Performance' o el título del canal + 'performance/daily',
      reemplaza por 'Rendimiento Diario 🔥'.
    - Si la primera línea es exactamente el nombre del canal, se elimina.
    - Limpia líneas vacías duplicadas.
    """
    if not text:
        return text

    lines = text.splitlines()
    if not lines:
        return text

    first = lines[0].strip()
    f_norm = _normalize(first)
    title_norm = _normalize(channel_title or "")

    # 1) Cualquier forma de "Daily Performance" en la primera línea
    if HEADER_DAILY_RE.match(first) or "rendimiento diario" in f_norm:
        lines[0] = "Rendimiento Diario 🔥"
    # 2) Si la primera línea contiene el nombre del canal y suena a resumen
    elif title_norm and title_norm in f_norm and ("performance" in f_norm or "daily" in f_norm or "resumen" in f_norm):
        lines[0] = "Rendimiento Diario 🔥"
    # 3) Si la primera línea es exactamente el nombre del canal, la quitamos
    elif title_norm and f_norm == title_norm:
        lines = lines[1:]

    # Limpieza de líneas vacías repetidas
    cleaned = []
    prev_blank = False
    for l in lines:
        if l.strip():
            cleaned.append(l)
            prev_blank = False
        else:
            if not prev_blank:
                cleaned.append(l)
                prev_blank = True

    return "\n".join(cleaned).strip()
# ---------------------------------------------------------------------


# ----------------- HTTP helpers con reintentos -----------------
def http_post_with_retries(url, *, headers=None, data=None, files=None, timeout=30, max_retries=HTTP_MAX_RETRIES):
    attempt = 0
    while True:
        try:
            r = requests.post(url, headers=headers, data=data, files=files, timeout=timeout)
            return r
        except Exception as e:
            attempt += 1
            if attempt >= max_retries:
                raise
            backoff = (HTTP_BACKOFF_BASE ** attempt)
            print(f"[{datetime.now()}] ⚠️ HTTP retry {attempt}/{max_retries} en {backoff:.1f}s por error: {e}")
            time.sleep(backoff)

def bot_send_photo_get_file_id(image_bytes: bytes, filename="photo.jpg"):
    if not BOT_TOKEN or not BOT_CHAT_ID:
        return None, "missing_bot_config"

    url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendPhoto"
    files = {"photo": (filename, image_bytes)}
    data = {"chat_id": BOT_CHAT_ID, "disable_notification": True}

    try:
        r = http_post_with_retries(url, data=data, files=files, timeout=30)
        j = r.json()
        if not j.get("ok"):
            return None, f"bot_api_error:{j}"
        sizes = j["result"]["photo"]
        file_id = sizes[-1]["file_id"]
        return file_id, None
    except Exception as e:
        return None, f"http_error:{e}"
# ---------------------------------------------------------------


# ----------------- Serializador de mensajes -----------------
def guess_image_ext(mime: str) -> str:
    if mime == "image/png":
        return ".png"
    if mime == "image/webp":
        return ".webp"
    if mime == "image/gif":
        return ".gif"
    # default
    return ".jpg"

async def serialize_message(client, message, channel_title: str):
    """
    Aplica filtro ANTES de descargar/subir media (para no spamear el bot).
    Si pasa el filtro, arma el payload y (si hay imagen) obtiene file_id.
    Sanitiza encabezados de resumen diario.
    """
    text = (getattr(message, "message", None) or getattr(message, "text", None) or "").strip()

    # ¿Hay media de imagen?
    has_media = False
    mime = None
    ext = ".jpg"

    if getattr(message, "photo", None):
        has_media = True
        mime = "image/jpeg"
        ext = ".jpg"
    elif getattr(message, "document", None):
        doc = message.document
        doc_mime = getattr(doc, "mime_type", "") or ""
        if doc_mime.startswith("image/"):
            has_media = True
            mime = doc_mime
            ext = guess_image_ext(doc_mime)

    # FILTRO primero (usa texto + si hay media)
    if not is_signal(text, has_media):
        return None

    # Si es texto, sanitiza encabezado
    if text:
        text = sanitize_text(text, channel_title)

    payload = {
        "id": message.id,
        "date": message.date.isoformat(),
    }
    if text:
        payload["text"] = text

    # Si no hay media, listo
    if not has_media:
        return payload

    # Si hay media: descargar y subir al bot para obtener file_id
    try:
        buf = BytesIO()
        await client.download_media(message, file=buf)
        data = buf.getvalue()
        buf.close()

        if data:
            filename = f"tg_{message.id}{ext}"
            file_id, err = bot_send_photo_get_file_id(data, filename=filename)
            if file_id:
                payload["media"] = {
                    "kind": "image",
                    "via": "bot_file_id",
                    "file_id": file_id
                }
            else:
                if len(data) <= MAX_INLINE_BYTES:
                    b64 = base64.b64encode(data).decode("ascii")
                    payload["media"] = {
                        "kind": "image",
                        "via": "base64_fallback",
                        "mime": mime or "image/jpeg",
                        "encoding": "base64",
                        "data": b64,
                        "filename": filename
                    }
                    if err:
                        payload["media_note"] = err
                else:
                    payload["media"] = {
                        "kind": "image",
                        "via": "skipped_too_large",
                        "approx_size_bytes": len(data),
                        "mime": mime or "image/jpeg",
                        "filename": filename
                    }
                    if err:
                        payload["media_note"] = err
    except FloodWaitError as fw:
        # Relevanta hacia arriba para que el loop superior lo maneje
        raise
    except Exception as e:
        payload["media_error"] = f"{type(e).__name__}: {e}"

    return payload
# -------------------------------------------------------------


async def main():
    client = TelegramClient(os.path.join(BASE_DIR, "mi_sesion"), api_id, api_hash)
    await client.start(phone_number)

    # Resolver el canal por link/@, con fallback por nombre
    target = await resolve_target(client)

    if not target:
        print("❌ Grupo/canal no encontrado.")
        await client.disconnect()
        return

    # 'target' YA ES ENTIDAD. Saca nombre seguro:
    title = get_display_name(target) or getattr(target, "title", None) or "Destino sin título"
    print(f"✅ Escuchando: {title}")

    # Cargar/guardar estado por canal
    state_path = state_path_for_target(target)
    last_id = load_last_id(state_path)

    while True:
        try:
            # Recorrer SOLO lo nuevo, en orden ascendente (reverse=True)
            async for message in client.iter_messages(target, min_id=last_id, reverse=True):
                payload = await serialize_message(client, message, title)

                # Si no pasa filtro, igual avanzamos puntero
                if payload is None:
                    last_id = message.id
                    save_last_id(state_path, last_id)
                    continue

                # Enviar al webhook con reintentos/backoff
                try:
                    r = http_post_with_retries(
                        webhook_url,
                        headers={'Content-Type': 'application/json'},
                        data=json.dumps(payload),
                        timeout=25
                    )
                    print(f"[{datetime.now()}] Enviado: {r.status_code} - {r.text[:180]}")
                except Exception as http_err:
                    print(f"[{datetime.now()}] ❌ Error HTTP definitivo: {http_err}")

                last_id = message.id
                save_last_id(state_path, last_id)

            await asyncio.sleep(POLL_SECONDS)

        except FloodWaitError as fw:
            secs = int(getattr(fw, "seconds", 30))
            print(f"[{datetime.now()}] ⏳ FloodWait: durmiendo {secs}s…")
            await asyncio.sleep(secs)
        except Exception as e:
            print(f"[{datetime.now()}] ❌ Error loop: {e}")
            await asyncio.sleep(POLL_SECONDS)

if __name__ == "__main__":
    asyncio.run(main())
