#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
================================================================================
    CAÇADOR DE TARTARUGAS V15.00
    - Anti-Handshake: Deteção real de login vs handshake DMSS
    - Relatório diário e semanal
    - Deteção de Playback via log.cgi com canal, data e período
    - Deteção de canal em LiveView via log.cgi
    - Deteção de PTZ, Export/Download e Snapshots
    - Persistência JSON, Recordes, Canais favoritos
    - Comandos Telegram: /status /stats /hoje /semana /heatmap /recordes /canais
    - Auto-restart de threads
================================================================================"""

import os
# Forçar timezone Europe/Lisbon ANTES de qualquer chamada a datetime.now()
# O cacador (host Proxmox) corre em UTC, mas o Zé trabalha em hora de Lisboa (UTC+1 verão).
# Sem isto, HORA_INICIO=08:30 seria 08:30 UTC = 09:30 Lisboa (atraso de 1h no início do turno).
os.environ['TZ'] = 'Europe/Lisbon'
import time
time.tzset()

import requests
from requests.auth import HTTPDigestAuth
import datetime
import threading
import urllib3
import re
import random
import json
import sys

# Forçar line-buffering no stdout para os prints aparecerem em tempo real no journalctl
# (caso contrário, systemd-journal capta tudo em bloco quando o buffer enche)
try:
    sys.stdout.reconfigure(line_buffering=True)
except Exception:
    pass

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# ==========================================
#  CONFIGURAÇÃO DE PRODUÇÃO
# ==========================================
GRAVADORES = [
    {"nome": "NVR Segunor", "ip": "192.168.10.150", "user": "bot", "pass": "Protek6201%"},
    {"nome": "NVR Protek", "ip": "192.168.10.151", "user": "bot", "pass": "Protek6201%"},
    {"nome": "NVR Palmeira", "ip": "10.0.50.223", "user": "bot", "pass": "Protek6201%"}
]

TELEGRAM_TOKEN = "8080703963:AAF-A8Ch6jHzXoTZVfq5zeExNNptooxuOWY"
TELEGRAM_CHAT_ID = "-1003630020228" 

# Utilizadores alvo POR NVR. No Palmeira (DHI-NVR2108HS-8P-4KS3) o Zé não tem conta local —
# acede via partilha cloud share do DMSS, que regista no NVR como "admin" vindo de IP loopback 127.0.0.2.
USERS_ALVO_POR_NVR = {
    "NVR Segunor": ["ze"],
    "NVR Protek": ["ze"],
    "NVR Palmeira": ["admin"],
}
# União global (compatibilidade com call sites legacy em /status, /hoje, /stats)
USERS_ALVO = sorted({u for users in USERS_ALVO_POR_NVR.values() for u in users})

# Timestamp de arranque do script (usado para ignorar sessões pré-existentes no Palmeira,
# como a sessão admin Dahua3.0 persistente que fica aberta entre reinícios do NVR)
SCRIPT_START_TIME = datetime.datetime.now()

HORA_INICIO = datetime.time(8, 30)
HORA_FIM = datetime.time(19, 30)

TEMPO_TEDIO = 5400

# ==========================================
#  CONFIGURAÇÕES DE DETEÇÃO AVANÇADA
# ==========================================
# Tempo mínimo (segundos) para considerar sessão como "login real" vs "handshake"
# DMSS faz handshake rápido (~1-3s) a todos os gravadores configurados
# Aumentado para 5s para maior segurança
TEMPO_MINIMO_SESSAO_REAL = 5.0

# Tempo de janela para agrupar logins/logouts do mesmo user
TEMPO_AGRUPAMENTO = 4.0

# Cooldown entre notificações de playback do mesmo user (evita spam)
PLAYBACK_NOTIFY_COOLDOWN = 300  # 5 minutos

# Tempo mínimo para confirmar sessão quando o mesmo user entra em MÚLTIPLOS NVRs
# simultaneamente (típico de handshake DMSS que mantém conexões persistentes)
TEMPO_MINIMO_MULTI_NVR = 30.0

# Alerta de sessão longa (em segundos)
TEMPO_SESSAO_LONGA = 600  # 10 minutos
TEMPO_SESSAO_LONGA_COOLDOWN = 600  # Não repetir alerta antes de mais 10 min

# Sessão MUITO longa (vigia obsessiva) — alerta especial em separado
TEMPO_SESSAO_MUITO_LONGA = 7200  # 2 horas
TEMPO_SESSAO_MUITO_LONGA_COOLDOWN = 3600  # repetir a cada 1h enquanto persistir

# Inatividade prolongada (Zé não aparece em horário laboral) — alerta de "esquecido"
INATIVIDADE_HORAS_ALERTA = 24  # avisar se passaram >24h sem acessos durante horário laboral
INATIVIDADE_COOLDOWN = 86400  # uma vez por dia

# Anomalia de horário: login em hora pouco frequente (<X% dos acessos)
ANOMALIA_HORA_THRESHOLD_PCT = 0.05  # hora com <5% do total = anómala
ANOMALIA_HORA_COOLDOWN = 1800  # 30 min entre alertas de hora anómala

# Alerta de RAJADA de exports (possível fuga de dados)
EXPORTS_RAJADA = 5  # 5 exports
EXPORTS_JANELA = 300  # em 5 min
EXPORTS_RAJADA_COOLDOWN = 600  # não repetir alerta antes de 10 min

# Persistência de dados
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_FILE = os.path.join(DATA_DIR, "tartaruga_data.json")
SAVE_INTERVAL = 300  # Guardar a cada 5 minutos
HISTORY_MAX_DAYS = 30  # Manter histórico de 30 dias

# ==========================================
#  ESTADO GLOBAL
# ==========================================
sessoes_ativas = {}         # {chave: {user, ip, first_seen, nvr_nome, confirmed, client_type}}
sessoes_pendentes = {}      # Sessões à espera de confirmação (anti-handshake)
nvr_status_cache = {}

# Estado de atividade (LiveView vs Playback)
user_activity = {}          # {user: {mode, last_update, nvr, last_playback_notify, canal, data_playback, periodo}}

# Deteção de operações especiais
user_operations = {}        # {user: {ptz_count, export_count, snapshot_count, last_ptz_notify, last_export_notify}}

# Contadores
missed_access_count_night = 0
daily_stats = {}
daily_time = {}  # Tempo total de visualização por NVR (em segundos)

# Histórico persistente
history = []  # [{date, stats: {nvr: count}, time: {nvr: seconds}}]

# Recordes
recordes = {
    'sessao_mais_longa': {'duracao': 0, 'nvr': '', 'data': ''},
    'dia_mais_acessos': {'total': 0, 'data': ''},
    'dia_mais_tempo': {'total': 0, 'data': ''},
}

# Canais favoritos de playback
canais_playback = {}  # {canal: count}

# Heat map horário (acessos por hora)
acessos_por_hora = {}  # {'09': 5, '10': 3, ...}

# Duração de playback
tempo_playback_total = 0.0  # segundos acumulados no dia
playback_start_time = {}  # {user: timestamp_inicio_playback}

# Alertas de sessão longa
long_session_alerted = {}  # {chave: last_alert_time}
very_long_session_alerted = {}  # {chave: last_alert_time} — sessão muito longa (>2h)

# Anomalias
inactivity_last_alert = 0  # timestamp último alerta de inatividade
anomalia_hora_last_alert = 0  # timestamp último alerta de hora anómala

# Rajada de exports (tracking rolling window por user)
exports_recentes = {}  # {user: [timestamps...]}
exports_rajada_last_alert = {}  # {user: last_alert_time}

# Diagnóstico Palmeira (Fase 1): rastreio de Ids já logged para evitar repetição
_palmeira_logged_ids = set()

sleep_mode_active = False
last_activity_time = time.time()

# Buffers
logout_buffer = {}
login_buffer = {}

# Anti-spam de notificações Telegram: após enviar login/logout user@NVR,
# suprimir notificações iguais durante NOTIFY_COOLDOWN segundos.
# NÃO afeta deteção (contadores, tempo, recordes continuam) — só silencia o Telegram.
NOTIFY_COOLDOWN = 30  # 30s — filtra ciclos rápidos do DMSS (~5-15s entre devices) mas detecta reentries genuínas
last_notify_at = {}  # {('login'|'logout', user, nvrs_str): timestamp}

# Thread registry para auto-restart
thread_registry = []  # [(target, args, kwargs, name)]

lock = threading.Lock()

# ==========================================
#  PERSISTÊNCIA DE DADOS
# ==========================================

def guardar_dados():
    """Guarda stats atuais e histórico em ficheiro JSON."""
    try:
        data = {
            "daily_stats": daily_stats,
            "daily_time": daily_time,
            "missed_access_count_night": missed_access_count_night,
            "history": history[-HISTORY_MAX_DAYS:],
            "recordes": recordes,
            "canais_playback": canais_playback,
            "acessos_por_hora": acessos_por_hora,
            "tempo_playback_total": tempo_playback_total,
            "last_save": datetime.datetime.now().isoformat()
        }
        tmp_file = DATA_FILE + ".tmp"
        with open(tmp_file, 'w', encoding='utf-8') as f:
            json.dump(data, f, indent=2, ensure_ascii=False)
        os.replace(tmp_file, DATA_FILE)
        ts = datetime.datetime.now().strftime('%H:%M:%S')
        print(f"[{ts}] 💾 Dados guardados ({len(history)} dias de histórico)")
    except Exception as e:
        ts = datetime.datetime.now().strftime('%H:%M:%S')
        print(f"[{ts}] [!] Erro ao guardar dados: {e}")


def carregar_dados():
    """Carrega dados persistentes do ficheiro JSON."""
    global daily_stats, daily_time, missed_access_count_night, history, recordes, canais_playback
    global acessos_por_hora, tempo_playback_total
    
    if not os.path.exists(DATA_FILE):
        print("[*] Sem dados anteriores. Começando do zero.")
        return
    
    try:
        with open(DATA_FILE, 'r', encoding='utf-8') as f:
            data = json.load(f)
        
        # Restaurar contadores do dia (se o script reiniciou no mesmo dia)
        last_save = data.get('last_save', '')
        if last_save:
            last_date = last_save[:10]
            today = datetime.datetime.now().strftime('%Y-%m-%d')
            
            if last_date == today:
                daily_stats.update(data.get('daily_stats', {}))
                daily_time.update({k: float(v) for k, v in data.get('daily_time', {}).items()})
                missed_access_count_night = data.get('missed_access_count_night', 0)
                print(f"[*] Dados do dia restaurados (guardados às {last_save[11:19]})")
                for nome in daily_stats:
                    print(f"    ↳ {nome}: {daily_stats[nome]} acessos, {formatar_tempo(daily_time.get(nome, 0))}")
            else:
                print(f"[*] Último save foi noutro dia ({last_date}). Contadores resetados.")
        
        # Restaurar histórico
        history.extend(data.get('history', []))
        seen = set()
        unique_history = []
        for entry in history:
            if entry['date'] not in seen:
                seen.add(entry['date'])
                unique_history.append(entry)
        history.clear()
        history.extend(sorted(unique_history, key=lambda x: x['date'])[-HISTORY_MAX_DAYS:])
        print(f"[*] Histórico carregado: {len(history)} dias")
        
        # Restaurar recordes
        saved_recordes = data.get('recordes', {})
        for key in recordes:
            if key in saved_recordes:
                recordes[key] = saved_recordes[key]
        if recordes['sessao_mais_longa']['duracao'] > 0:
            print(f"[*] Recorde sessão: {formatar_tempo(recordes['sessao_mais_longa']['duracao'])} ({recordes['sessao_mais_longa']['data']})")
        
        # Restaurar canais favoritos
        canais_playback.update(data.get('canais_playback', {}))
        if canais_playback:
            top = sorted(canais_playback.items(), key=lambda x: x[1], reverse=True)[:3]
            print(f"[*] Canais favoritos: {', '.join(f'CH{k}({v}x)' for k, v in top)}")
        
        # Restaurar heatmap e playback (se mesmo dia)
        if last_save and last_save[:10] == datetime.datetime.now().strftime('%Y-%m-%d'):
            acessos_por_hora.update(data.get('acessos_por_hora', {}))
            tempo_playback_total = data.get('tempo_playback_total', 0.0)
            if acessos_por_hora:
                print(f"[*] Heatmap restaurado: {sum(acessos_por_hora.values())} acessos por hora")
            if tempo_playback_total > 0:
                print(f"[*] Playback acumulado: {formatar_tempo(tempo_playback_total)}")
        
    except Exception as e:
        print(f"[!] Erro ao carregar dados: {e}")


def atualizar_recorde_sessao(duracao, nvr_nome):
    """Verifica e atualiza recorde de sessão mais longa."""
    if duracao > recordes['sessao_mais_longa']['duracao']:
        data_str = datetime.datetime.now().strftime('%Y-%m-%d')
        old = recordes['sessao_mais_longa']['duracao']
        recordes['sessao_mais_longa'] = {
            'duracao': duracao,
            'nvr': nvr_nome,
            'data': data_str
        }
        ts = datetime.datetime.now().strftime('%H:%M:%S')
        if old > 0:
            print(f"[{ts}] 🏆 NOVO RECORDE de sessão: {formatar_tempo(duracao)} (anterior: {formatar_tempo(old)})")
        return True
    return False


def atualizar_recordes_diarios():
    """Verifica e atualiza recordes diários (acessos e tempo)."""
    total_acessos = sum(daily_stats.values())
    total_tempo = sum(daily_time.values())
    data_str = datetime.datetime.now().strftime('%Y-%m-%d')
    
    novo_recorde = False
    
    if total_acessos > recordes['dia_mais_acessos']['total']:
        recordes['dia_mais_acessos'] = {'total': total_acessos, 'data': data_str}
        novo_recorde = True
    
    if total_tempo > recordes['dia_mais_tempo']['total']:
        recordes['dia_mais_tempo'] = {'total': total_tempo, 'data': data_str}
        novo_recorde = True
    
    return novo_recorde


def gerar_texto_recordes():
    """Gera texto formatado com os recordes atuais."""
    r = recordes
    partes = ["🏆 *RECORDES DA TARTARUGA*\n"]
    
    if r['sessao_mais_longa']['duracao'] > 0:
        partes.append(f"⏱️ Sessão mais longa: *{formatar_tempo(r['sessao_mais_longa']['duracao'])}*")
        partes.append(f"   📍 {r['sessao_mais_longa']['nvr']} ({r['sessao_mais_longa']['data']})")
    else:
        partes.append("⏱️ Sessão mais longa: _sem registo_")
    
    if r['dia_mais_acessos']['total'] > 0:
        partes.append(f"\n🔢 Dia com mais acessos: *{r['dia_mais_acessos']['total']}* ({r['dia_mais_acessos']['data']})")
    
    if r['dia_mais_tempo']['total'] > 0:
        partes.append(f"🕐 Dia com mais tempo: *{formatar_tempo(r['dia_mais_tempo']['total'])}* ({r['dia_mais_tempo']['data']})")
    
    return "\n".join(partes)


def gerar_texto_canais():
    """Gera texto formatado com os canais favoritos."""
    if not canais_playback:
        return "📹 *CANAIS FAVORITOS*\n\n_Sem dados de playback ainda._"
    
    sorted_canais = sorted(canais_playback.items(), key=lambda x: x[1], reverse=True)
    total = sum(v for _, v in sorted_canais)
    
    partes = ["📹 *CANAIS FAVORITOS DE PLAYBACK*\n"]
    for i, (canal, count) in enumerate(sorted_canais[:10]):
        pct = (count / total * 100) if total > 0 else 0
        medal = ["🥇", "🥈", "🥉"][i] if i < 3 else f"{i+1}."
        bar = "█" * max(1, int(pct / 5))
        partes.append(f"{medal} Canal {canal}: *{count}x* ({pct:.0f}%) {bar}")
    
    partes.append(f"\n📊 Total de playbacks: {total}")
    return "\n".join(partes)


def gerar_relatorio_semanal():
    """Gera relatório semanal com estatísticas da semana."""
    # Últimos 5 dias úteis (segunda a sexta)
    ultimos = history[-5:] if len(history) >= 5 else history
    if not ultimos:
        return "📊 *RELATÓRIO SEMANAL*\n\n_Sem dados suficientes para relatório._"
    
    total_acessos = 0
    total_tempo = 0
    dia_mais_ativo = {'date': '', 'total': 0}
    dia_mais_tempo = {'date': '', 'total': 0}
    
    partes = ["📊 *RELATÓRIO SEMANAL*\n"]
    partes.append("📅 *Resumo diário:*")
    
    for dia in ultimos:
        acessos = sum(dia.get('stats', {}).values())
        tempo = sum(dia.get('time', {}).values())
        total_acessos += acessos
        total_tempo += tempo
        
        # Formatar dia da semana
        try:
            dt = datetime.datetime.strptime(dia['date'], '%Y-%m-%d')
            dia_semana = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'][dt.weekday()]
        except:
            dia_semana = '---'
        
        bar = "🟩" * max(0, min(10, acessos))
        partes.append(f"  {dia_semana} {dia['date'][5:]}: {acessos} acessos, {formatar_tempo(tempo)} {bar}")
        
        if acessos > dia_mais_ativo['total']:
            dia_mais_ativo = {'date': dia['date'], 'total': acessos}
        if tempo > dia_mais_tempo['total']:
            dia_mais_tempo = {'date': dia['date'], 'total': tempo}
    
    n = len(ultimos)
    partes.append(f"\n📈 *Totais da semana:*")
    partes.append(f"  🔢 Acessos: {total_acessos} (média: {total_acessos/n:.1f}/dia)")
    partes.append(f"  ⏱️ Tempo: {formatar_tempo(total_tempo)} (média: {formatar_tempo(total_tempo/n)}/dia)")
    
    if dia_mais_ativo['total'] > 0:
        partes.append(f"  🏆 Dia mais ativo: {dia_mais_ativo['date']} ({dia_mais_ativo['total']} acessos)")
    
    # Canais favoritos
    if canais_playback:
        top3 = sorted(canais_playback.items(), key=lambda x: x[1], reverse=True)[:3]
        canais_str = ", ".join(f"CH{k} ({v}x)" for k, v in top3)
        partes.append(f"\n📹 *Canais mais vigiados:* {canais_str}")
    
    # Recordes
    r = recordes
    if r['sessao_mais_longa']['duracao'] > 0:
        partes.append(f"\n🏆 *Recorde sessão:* {formatar_tempo(r['sessao_mais_longa']['duracao'])} ({r['sessao_mais_longa']['data']})")
    
    # Frase de fecho
    frases_fecho = [
        "Bom fim de semana! A tartaruga que descanse… se conseguir. 🐢",
        "Semana encerrada. O Zé que aproveite o descanso! 🐢",
        "Relatório feito. Agora é folga até segunda! 🐢",
        "Mais uma semana de vigilância concluída. O Zé é incansável! 🐢",
        "Fim de semana à vista. O caçador vai descansar! 🐢",
    ]
    partes.append(f"\n_{random.choice(frases_fecho)}_")
    
    return "\n".join(partes)


def gerar_frase_playback_contextual(hora_inicio, hora_fim, data_playback=None):
    """Gera frase contextual baseada no horário e data que está a ser revisto no playback."""
    # Aceita formatos "HH:MM", "HH.MM", "HH", etc. — extrai a primeira sequência de 1-2 dígitos.
    m_ini = re.match(r'(\d{1,2})', hora_inicio or '')
    m_fim = re.match(r'(\d{1,2})', hora_fim or '')
    if not (m_ini and m_fim):
        return None
    h_inicio = int(m_ini.group(1))
    h_fim = int(m_fim.group(1))

    # Frases extra baseadas na data
    prefixo_data = ""
    if data_playback:
        try:
            dt_pb = datetime.datetime.strptime(data_playback, '%Y-%m-%d')
            dias_atras = (datetime.datetime.now() - dt_pb).days
            dia_semana = dt_pb.weekday()
            if dias_atras == 0:
                prefixo_data = ""  # Hoje, sem prefixo extra
            elif dias_atras == 1:
                prefixo_data = random.choice([
                    "A rever o dia de ontem. ",
                    "Ontem não chegou? A confirmar agora. ",
                    "Replay de ontem. ",
                ])
            elif dia_semana >= 5:  # Fim de semana
                prefixo_data = random.choice([
                    "A ver o FIM DE SEMANA?! Paranoia total. ",
                    "Replay do fim de semana. Está desconfiado de algo. ",
                    "A ver o que aconteceu no sábado/domingo! ",
                ])
            elif dias_atras > 7:
                prefixo_data = random.choice([
                    f"A ver gravações de há {dias_atras} dias! Investigação a sério. ",
                    f"Replay de há {dias_atras} dias. O Zé é persistente. ",
                    f"A cavar no passado ({dias_atras} dias atrás). ",
                ])
            elif dias_atras > 1:
                prefixo_data = random.choice([
                    f"A rever há {dias_atras} dias. ",
                    f"Replay de {data_playback[5:]}. ",
                ])
        except:
            pass
    
    # Madrugada (00-06)
    if h_inicio >= 0 and h_fim <= 6:
        frases = [
            "🌙 A ver a madrugada? Está à procura de fantasmas!",
            "🌙 Imagens da madrugada… Suspeita de algo.",
            "🌙 A vasculhar a noite. O Zé vira detetive noturno.",
            "🌙 Madrugada no replay. Quem anda a fazer horas extra?",
            "🌙 Está a ver as câmaras de madrugada. Modo CSI ativado.",
        ]
    # Chegada/manhã (07-09)
    elif h_inicio >= 7 and h_inicio <= 9:
        frases = [
            "☕ A fiscalizar quem chegou atrasado…",
            "☕ A ver a hora de chegada. O Diogo que se cuide.",
            "☕ A conferir pontualidade. Alguém vai ter problemas.",
            "☕ Replay da manhã. A caçar atrasados!",
            "☕ A verificar entradas matinais. Modo fiscal ativado.",
            "☕ O Zé está a ver se o Diogo chegou a horas…",
            "☕ A checar o relógio de ponto virtual.",
        ]
    # Meio da manhã (10-11)
    elif h_inicio >= 10 and h_inicio <= 11:
        frases = [
            "📋 A rever a manhã. Alguém esteve a coçar?",
            "📋 Replay das 10-11h. A ver quem faz intervalo longo.",
            "📋 O Zé está a fiscalizar o meio da manhã.",
            "📋 Verificação matinal. A ver se o Diogo estava no lugar.",
        ]
    # Almoço (12-14)
    elif (h_inicio >= 12 and h_inicio <= 14) or (h_fim >= 12 and h_fim <= 14):
        frases = [
            "🍽️ Está a ver a hora de almoço! Andou alguém a comer demais?",
            "🍽️ A fiscalizar almoços. Quem demorou mais que 1 hora?",
            "🍽️ Replay do almoço. O Zé quer saber quem almoça à grande.",
            "🍽️ A ver quem passou o horário de almoço. Tribunal da fome.",
            "🍽️ Fiscalização gastronómica em curso.",
            "🍽️ A caça à pausa prolongada de almoço.",
            "🍽️ O Zé quer saber quem comeu pipocas no lugar.",
            "🍽️ Almoço sob vigilância. O cuscuz está a cuscar.",
        ]
    # Tarde (15-17)
    elif h_inicio >= 15 and h_inicio <= 17:
        frases = [
            "🌤️ A rever a tarde. Quem andou a dormir?",
            "🌤️ Replay da tarde. A hora da preguiça.",
            "🌤️ O Zé está a ver quem dormiu depois do almoço.",
            "🌤️ Vigilância da soneca pós-almoço ativada.",
        ]
    # Saída (17-19)
    elif h_inicio >= 17 and h_inicio <= 19:
        frases = [
            "🏃 A ver quem saiu mais cedo!",
            "🏃 Replay da hora de saída. Quem fugiu primeiro?",
            "🏃 O Zé está a fiscalizar as saídas. Alguém picou cedo.",
            "🏃 A conferir quem fez a corrida para o parque.",
            "🏃 Hora de saída sob vigilância. Quem saltou o muro?",
        ]
    # Noite (20-23)
    elif h_inicio >= 20:
        frases = [
            "🌃 A ver o que aconteceu à noite. Medo.",
            "🌃 Replay noturno. O Zé procura fantasmas.",
            "🌃 Imagens da noite. Isto é vigilância ou paranoia?",
        ]
    else:
        if prefixo_data:
            return prefixo_data + random.choice([
                "A investigar o passado.",
                "O detetive está em ação.",
                "Replay em andamento.",
            ])
        return None

    frase = random.choice(frases)
    if prefixo_data:
        frase = prefixo_data + frase.lstrip("🌙☕📋🍽️🌤️🏃🌃 ")
    return frase


def extrair_horario_playback(detail_text):
    """Extrai data e horário inicial/final do filename no detalhe do log.
    Formato: Nome do Ficheiro:2026-03-09  13.00.00---2026-03-09  14.02.46[R] dav
    Retorna (data_inicio, hora_inicio, hora_fim) ou (None, None, None)
    """
    # Padrão 1: data  HH.MM.SS---data  HH.MM.SS (formato Dahua com pontos)
    match = re.search(r'(\d{4}-\d{2}-\d{2})\s+(\d{2})[.](\d{2})[.](\d{2})---(\d{4}-\d{2}-\d{2})\s+(\d{2})[.](\d{2})[.](\d{2})', detail_text)
    if match:
        data_inicio = match.group(1)
        h_start = f"{match.group(2)}:{match.group(3)}"
        h_end = f"{match.group(6)}:{match.group(7)}"
        return data_inicio, h_start, h_end

    # Padrão 2: data HH:MM:SS---data HH:MM:SS (formato com dois-pontos)
    match = re.search(r'(\d{4}-\d{2}-\d{2})\s+(\d{2}):(\d{2}):\d{2}---(\d{4}-\d{2}-\d{2})\s+(\d{2}):(\d{2}):\d{2}', detail_text)
    if match:
        data_inicio = match.group(1)
        h_start = f"{match.group(2)}:{match.group(3)}"
        h_end = f"{match.group(5)}:{match.group(6)}"
        return data_inicio, h_start, h_end

    # Padrão 3: data/HH/MM/SS---data/HH/MM/SS (formato com barras)
    match = re.search(r'(\d{4}[/-]\d{2}[/-]\d{2})\s*[T ]?(\d{2})[/.](\d{2})[/.](\d{2}).*?---.*?(\d{4}[/-]\d{2}[/-]\d{2})\s*[T ]?(\d{2})[/.](\d{2})[/.](\d{2})', detail_text)
    if match:
        data_inicio = match.group(1).replace('/', '-')
        h_start = f"{match.group(2)}:{match.group(3)}"
        h_end = f"{match.group(6)}:{match.group(7)}"
        return data_inicio, h_start, h_end

    # Padrão 4: StartTime=... EndTime=... (formato CGI)
    start_match = re.search(r'[Ss]tart[Tt]ime[=:]?\s*(\d{4}-\d{2}-\d{2})\s+(\d{2}):(\d{2})', detail_text)
    end_match = re.search(r'[Ee]nd[Tt]ime[=:]?\s*\d{4}-\d{2}-\d{2}\s+(\d{2}):(\d{2})', detail_text)
    if start_match and end_match:
        data_inicio = start_match.group(1)
        h_start = f"{start_match.group(2)}:{start_match.group(3)}"
        h_end = f"{end_match.group(1)}:{end_match.group(2)}"
        return data_inicio, h_start, h_end

    # Logging defensivo: capturar formato real para iteração posterior
    if detail_text and detail_text.strip():
        print(f"[PLAYBACK?] Horário não extraído de: {detail_text[:300]}")
    return None, None, None


def extrair_canal_detalhe(detail_text):
    """Extrai número do canal do detalhe do log (suporta PT/EN, vários formatos)."""
    patterns = [
        r'[Cc]anal\s*[:=]\s*(\d+)',           # Canal:1, canal: 2, Canal=3
        r'[Cc]hannel\s*[:=]\s*(\d+)',          # Channel:1, channel: 2
        r'\bCH\s*[:=]?\s*(\d+)',               # CH:1, CH 1, CH1
        r'[Cc]am(?:era)?\s*[:=]?\s*(\d+)',     # Cam:1, Camera:1, cam 1
        r'[Cc]anal\s+(\d+)',                   # Canal 1
        r'[Cc]hannel\s+(\d+)',                 # Channel 1
        r'/cam/realmonitor\?channel=(\d+)',    # URL stream pattern
        r'channel[=](\d+)',                    # channel=1 (query string)
    ]
    for pattern in patterns:
        match = re.search(pattern, detail_text)
        if match:
            return match.group(1)
    # Logging defensivo: capturar formato real para futura iteração
    if detail_text and detail_text.strip():
        print(f"[CANAL?] Não extraído de: {detail_text[:200]}")
    return None


def gerar_heatmap_texto():
    """Gera visualização de heatmap horário em texto."""
    if not acessos_por_hora:
        return ""
    
    horas_ordenadas = sorted(acessos_por_hora.items(), key=lambda x: x[0])
    max_val = max(v for _, v in horas_ordenadas) if horas_ordenadas else 1
    
    partes = ["\n🕐 *Atividade por hora:*"]
    for hora, count in horas_ordenadas:
        bar_len = max(1, int(count / max_val * 8))
        bar = "█" * bar_len
        partes.append(f"  `{hora}h` {bar} {count}")
    
    hora_pico = max(acessos_por_hora.items(), key=lambda x: x[1])
    partes.append(f"  ⚡ Pico: *{hora_pico[0]}h* ({hora_pico[1]} acessos)")
    
    return "\n".join(partes)


def calcular_stats_janela(inicio_idx, fim_idx):
    """Calcula totais de uma janela de dias do history (slice [inicio_idx:fim_idx])."""
    if inicio_idx < 0 or fim_idx > len(history) or inicio_idx >= fim_idx:
        return None
    dias = history[inicio_idx:fim_idx]
    if not dias:
        return None
    total_acessos = 0
    total_tempo = 0.0
    por_nvr = {}
    for dia in dias:
        for nvr, count in dia.get('stats', {}).items():
            por_nvr.setdefault(nvr, {'acessos': 0, 'tempo': 0.0})
            por_nvr[nvr]['acessos'] += count
            total_acessos += count
        for nvr, tempo in dia.get('time', {}).items():
            por_nvr.setdefault(nvr, {'acessos': 0, 'tempo': 0.0})
            por_nvr[nvr]['tempo'] += tempo
            total_tempo += tempo
    return {
        'dias': len(dias),
        'total_acessos': total_acessos,
        'total_tempo': total_tempo,
        'por_nvr': por_nvr,
    }


def gerar_comparacao_semanal():
    """Compara últimos 7 dias vs 7 dias anteriores."""
    if len(history) < 14:
        return f"📊 *COMPARAÇÃO SEMANAL*\n\n_Histórico insuficiente: {len(history)} dias (mínimo 14)._"

    atual = calcular_stats_janela(len(history) - 7, len(history))
    anterior = calcular_stats_janela(len(history) - 14, len(history) - 7)
    if not (atual and anterior):
        return "📊 *COMPARAÇÃO SEMANAL*\n\n_Erro a calcular janelas._"

    def diff_text(a, b, is_tempo=False):
        diff = a - b
        if b == 0:
            return "—" if diff == 0 else "✨ (novo)"
        pct = (diff / b) * 100
        seta = "📈" if diff > 0 else ("📉" if diff < 0 else "➡️")
        sign = "+" if diff > 0 else ""
        if is_tempo:
            return f"{seta} {sign}{int(pct)}% ({sign}{formatar_tempo(diff)})"
        return f"{seta} {sign}{int(pct)}% ({sign}{int(diff)})"

    partes = [f"📊 *COMPARAÇÃO SEMANAL*\n"]
    partes.append(f"📅 *Esta semana* (últimos {atual['dias']}d): {atual['total_acessos']} acessos | {formatar_tempo(atual['total_tempo'])}")
    partes.append(f"📅 *Anterior* ({anterior['dias']}d antes): {anterior['total_acessos']} acessos | {formatar_tempo(anterior['total_tempo'])}\n")
    partes.append(f"*Variação total:*")
    partes.append(f"  🔢 Acessos: {diff_text(atual['total_acessos'], anterior['total_acessos'])}")
    partes.append(f"  ⏱️ Tempo: {diff_text(atual['total_tempo'], anterior['total_tempo'], is_tempo=True)}\n")

    todos_nvrs = sorted(set(atual['por_nvr']) | set(anterior['por_nvr']))
    if todos_nvrs:
        partes.append(f"*Por NVR (acessos):*")
        for nvr in todos_nvrs:
            a = atual['por_nvr'].get(nvr, {'acessos': 0, 'tempo': 0.0})
            b = anterior['por_nvr'].get(nvr, {'acessos': 0, 'tempo': 0.0})
            partes.append(f"  • {nvr}: {b['acessos']} → {a['acessos']} {diff_text(a['acessos'], b['acessos'])}")
    return "\n".join(partes)


def calcular_media_semanal():
    """Calcula a média de acessos e tempo dos últimos 7 dias úteis."""
    if not history:
        return None
    
    # Últimos 7 registos (dias úteis)
    ultimos = history[-7:] if len(history) >= 7 else history
    
    if not ultimos:
        return None
    
    total_acessos = {}
    total_tempo = {}
    
    for dia in ultimos:
        for nvr, count in dia.get('stats', {}).items():
            total_acessos[nvr] = total_acessos.get(nvr, 0) + count
        for nvr, tempo in dia.get('time', {}).items():
            total_tempo[nvr] = total_tempo.get(nvr, 0) + tempo
    
    n = len(ultimos)
    media_acessos = {nvr: total / n for nvr, total in total_acessos.items()}
    media_tempo = {nvr: total / n for nvr, total in total_tempo.items()}
    
    return {
        'dias': n,
        'media_acessos': media_acessos,
        'media_tempo': media_tempo
    }


def gerar_comparacao_media(protek_count, segunor_count, tempo_protek, tempo_segunor, palmeira_count=0, tempo_palmeira=0):
    """Gera texto comparando o dia atual com a média semanal."""
    media = calcular_media_semanal()
    if not media or media['dias'] < 3:
        return ""  # Precisa de pelo menos 3 dias de dados

    total_hoje = protek_count + segunor_count + palmeira_count
    media_total = sum(media['media_acessos'].values())

    tempo_hoje = tempo_protek + tempo_segunor + tempo_palmeira
    media_tempo_total = sum(media['media_tempo'].values())
    
    partes = []
    
    # Comparar acessos
    if media_total > 0:
        ratio = total_hoje / media_total
        if ratio >= 2.0:
            partes.append(f"📈 Hoje cuscou {ratio:.1f}x mais que a média ({int(media_total)} acessos/dia)!")
        elif ratio >= 1.5:
            partes.append(f"📈 Dia agitado: +{int((ratio-1)*100)}% acima da média.")
        elif ratio <= 0.3 and total_hoje > 0:
            partes.append(f"📉 Dia calmo: só {int(ratio*100)}% da média habitual.")
        elif ratio <= 0.5:
            partes.append(f"📉 Metade dos acessos habituais. Dia de folga?")
    
    # Comparar tempo
    if media_tempo_total > 60:  # Só se a média for > 1 min
        if tempo_hoje > 0:
            ratio_t = tempo_hoje / media_tempo_total
            if ratio_t >= 2.0:
                partes.append(f"⏱️ Esteve colado {ratio_t:.1f}x mais tempo que o habitual!")
            elif ratio_t <= 0.3:
                partes.append(f"⏱️ Passou pouco tempo a cuscar hoje.")
    
    if not partes:
        partes.append(f"📊 Dentro da média (média: {int(media_total)} acessos/dia em {media['dias']} dias).")
    
    return "\n".join(partes)


# ==========================================
#  INICIALIZAÇÃO
# ==========================================
for g in GRAVADORES:
    nvr_status_cache[g['nome']] = True
    daily_stats[g['nome']] = 0
    daily_time[g['nome']] = 0.0

def print_banner():
    print(r"""
================================================================================
        CAÇADOR DE TARTARUGAS V15.00
================================================================================
  [✓] Anti-Handshake: {}s mínimo para confirmar sessão
  [✓] Relatório diário + semanal (sextas)
  [✓] Deteção de Playback via log.cgi com canal, data e período
  [✓] Deteção de canal em LiveView via log.cgi
  [✓] Deteção de PTZ, Export/Download e Snapshots
  [✓] Persistência JSON, Recordes, Canais favoritos
  [✓] Comandos Telegram: /status /stats /hoje /semana /heatmap /recordes /canais
  [✓] Auto-restart de threads
  [✓] Heatmap horário + Duração de playback
================================================================================
    """.format(TEMPO_MINIMO_SESSAO_REAL))
    # Estado de arranque detalhado para debug em journalctl
    print(f"[BOOT] Script arrancou em: {SCRIPT_START_TIME.strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"[BOOT] NVRs configurados ({len(GRAVADORES)}):")
    for g in GRAVADORES:
        alvos = USERS_ALVO_POR_NVR.get(g['nome'], USERS_ALVO)
        print(f"[BOOT]   - {g['nome']:15s} @ {g['ip']:18s} | alvos: {alvos}")
    print(f"[BOOT] USERS_ALVO (união): {USERS_ALVO}")
    print(f"[BOOT] Horário ativo: {HORA_INICIO}-{HORA_FIM} | TEMPO_TEDIO={TEMPO_TEDIO}s")


# ==========================================
#  GERADORES DE TEXTO - VERSÃO REDUZIDA
#  (Adiciona as tuas frases completas aqui)
# ==========================================

def gerar_titulo_alerta():
    titulos = [
         "🐢 ALVO DETETADO",
        "🐢 OLHA QUEM É ELE!",
        "🐢 TARTARUGA À VISTA!",
        "🐢 O NINJA APARECEU!",
        "🐢 ALERTA DE LENTIDÃO!",
        "🐢 FINALMENTE SAIU DA TOCA!",
        "🐢 CUIDADO, ELE VEM AÍ!",
        "🐢 MOVIMENTO LENTO DETETADO",
        "🐢 SAI DA FRENTE GUEDES!",
        "🐢 CAÇA GROSSA NO RADAR",
        "🐢 ATENÇÃO: MOVIMENTO SUSPEITO",
        "🐢 O CHEFE CHEGOU (OU SAIU)",
        "🐢 DETEÇÃO DE CARAPAÇA",
        "🐢 ALERTA CM: ÚLTIMA HORA",
        "🐢 BREAKING NEWS: TARTARUGA",
        "🐢 SENTIDO! CHEFE NO RECINTO",
        "🐢 LUZ, CÂMARA, AÇÃO!",
        "🐢 PREPAREM AS CÂMARAS",
        "🐢 VEM AÍ ASNEIRA",
        "🐢 CONFIRMADO: É O ZÉ",
        "🐢 ALVO NA MIRA",
        "🐢 PIZZA TIME? (Cowabunga!)",
        "🐢 O MESTRE ROSHI ESTÁ AQUI",
        "🐢 BIP BIP! TARTARUGA DETETADA",
        "🐢 RADAR DISPAROU",
        "🐢 A TARTARUGA SAIU DA CASCA",
        "🐢 ATIVIDADE PARANORMAL DETETADA",
        "🐢 MEXEU-SE! MILAGRE!",
        "🐢 OLHA O PASSARINHO (TARTARUGA)",
        "🐢 ELAS ANDEM AÍ...",
        "🐢 ALERTA VERMELHO",
        "🐢 HOUSTON, TEMOS UMA TARTARUGA",
        "🐢 ALERTA SÍSMICO: PASSOS PESADOS",
        "🐢 O REI DA FESTA CHEGOU",
        "🐢 CUIDADO COM O VASO!",
        "🐢 ALGUÉM ACORDOU A BESTA",
        "🐢 STATUS: TARTARUGA ONLINE",
        "🐢 ATENÇÃO: NÍVEIS DE PREGUIÇA A DESCER",
        "🐢 OLHA O BICHINHO A MEXER",
        "🐢 ALERTA DE PRODUTIVIDADE (DUVIDOSA)",
        "🐢 INTERPOL AVISA: ZÉ ONLINE",
        "🐢 A LENDA ESTÁ VIVA",
        "🐢 NÃO É UM PÁSSARO, É O ZÉ",
        "🐢 CORRAM PARA AS COLINAS",
        "🐢 O GRANDE IRMÃO ESTÁ A VER",
        "🐢 ESPIÃO MESTRE DETETADO",
        "🐢 ALERTA DE CARISMA",
        "🐢 O ZÉ LIGOU O TURBO (NOT)",
        "🐢 DETETADO OBJETO NÃO IDENTIFICADO",
        "🐢 ATENÇÃO ÀS CARTEIRAS",
        "🐢 O CAPITÃO IGLO CHEGOU",
        "🐢 SILÊNCIO, QUE ELE VAI FALAR",
        "🐢 A TARTARUGA SAIU DO SOFÁ",
        "🐢 ALERTA CMTV: DIRETO",
        "🐢 PAREM AS MÁQUINAS!",
        "🐢 O ZÉ ESTÁ NA ÁREA",
        "🐢 TARGET ACQUIRED: GENIAL",
        "🐢 QUEM É VIVO SEMPRE APARECE",
        "🐢 OBRIGADO POR VIRES, ZÉ",
        "🐢 JÁ NÃO ERA SEM TEMPO",
        "🐢 CONTACTO VISUAL CONFIRMADO",
        "🐢 O DRONE DETETOU O ALVO",
        "🐢 SNIPER PRONTO (A BRINCAR)",
        "🐢 SISTEMA DE MÍSSEIS ATIVADO",
        "🐢 ALERTA DE FOFOCA",
        "🐢 O INSPETOR GADGET CHEGOU",
        "🐢 A TARTARUGA VAI ATACAR",
        "🐢 MODERADAMENTE PERIGOSO",
        "🐢 ALERTA DE SONO",
        "🐢 ESTÁ VIVO! ESTÁ VIVO!",
        "🐢 O ZÉ SAIU DA HIBERNAÇÃO",
        "🐢 ALERTA DEPA: ZÉ NA ZONA",
        "🐢 O GIGANTE ACORDOU",
        "🐢 ALERTA DE CARAPAÇA RIJA",
        "🐢 LÁ VEM ELE...",
        "🐢 FECHEM AS PORTAS",
        "🐢 ATIVIDADE SUSPEITA NO SETOR 7",
        "🐢 ZÉ_OS_OBSERVADOR.EXE",
        "🐢 ELE VÊ TUDO",
        "🐢 O VIGILANTE FOI VIGIADO",
        "🐢 CAÇA À TARTARUGA: SUCESSO",
        "🐢 ALVO BLOQUEADO",
        "🐢 SISTEMA COMPROMETIDO PELO ZÉ",
        "🐢 MOVIMENTO DETETADO (FINALMENTE)",
    "🐢 ALERTA: A TARTARUGA MEXEU UM POUCO",
    "🐢 SENHORAS E SENHORES… ELE LEVANTOU-SE",
    "🐢 ATENÇÃO: CARAPAÇA EM DESLOCAÇÃO",
    "🐢 EVENTO RARO: ZÉ EM MOVIMENTO",
    "🐢 ALERTA MUNDIAL: O ZÉ LEVANTOU O RABO",
    "🐢 CONFIRMADO: NÃO É UM BUG, É O ZÉ",
    "🐢 A LENDA VOLTOU A MEXER",
    "🐢 ALERTA DE BAIXA VELOCIDADE EXTREMA",
    "🐢 O MILAGRE ACONTECEU",
    "🐢 O NVR NÃO ESTAVA A SONHAR",
    "🐢 ALERTA: VIDA INTELIGENTE DETETADA",
    "🐢 BREAKING: O ZÉ SAIU DO MODO ESTÁTUA",
    "🐢 ATENÇÃO: PASSOS CONTADOS A DEDO",
    "🐢 O ALVO ATREVEU-SE A MEXER",
    "🐢 ALERTA: A TARTARUGA NÃO ESTÁ A DORMIR",
    "🐢 O SISTEMA ACORDEU COM O SUSTO",
    "🐢 MOVIMENTO REGISTADO (CHAMEM A CMTV)",
    "🐢 ALERTA: O ZÉ NÃO ESTÁ NO SOFÁ",
    "🐢 CONFIRMADO: EXISTE VIDA NA CARAPAÇA",
            "A olhar para as câmaras em direto 📺",
        "Em modo vigilante - LiveView ativo 👀",
        "A ver o que se passa ao vivo 📡",
        "Olhos nas câmaras (tempo real) 🔴",
        "A fazer de Big Brother em direto 🎬",
                "A olhar para as câmaras em direto 📺 (tipo stalker legal)",
        "LiveView ativo 👀 a ver se ele mexe o cu",
        "Em direto a apanhar preguiça em 4K 🔴",
        "Modo Big Brother ligado: Zé Edition 🎬",
        "A ver ao vivo quem está a fingir que trabalha 📡",
        "LiveView ON. Vergonha alheia pronta.",
        "Em tempo real a observar a arte de não fazer um caralho",
        "Câmaras ligadas. O Zé que se porte mal.",
        "Olhos colados ao ecrã à espera de um milagre",
        "LiveView ativo: produtividade sob vigilância",
        "A ver em direto… infelizmente",
        "Modo voyeur profissional ativado 👀",
        "Zé em LiveView 👀 a ver quem está parado",
        "CCTV em direto: controlo de almoços ativo",
        "Modo Big Brother ligado 📺",
        "LiveView ativo. Fingir que trabalham, rápido.",
        "Zé online a julgar pessoas em silêncio",
        "Em direto para ver quem chegou tarde",
        "Câmaras ligadas. Pressão psicológica ativada.",
        "LiveView ON. O fiscal está a ver.",
        "Zé a passear pelas câmaras em tempo real",
        "Reality show corporativo em direto",
        "Modo cusco profissional ativado",
        "LiveView ativo. Quem pisca perde."
    ]
    return random.choice(titulos)

def gerar_frase_local(nvrs_list):
    """Gera frase contextual baseada nos NVRs acedidos. Retorna string ou vazio."""
    nomes = [n.upper() for n in nvrs_list]
    tem_palmeira = any("PALMEIRA" in n for n in nomes)
    tem_protek = any("PROTEK" in n for n in nomes)
    tem_segunor = any("SEGUNOR" in n for n in nomes)

    if tem_palmeira and tem_protek and tem_segunor:
        frases = [
            "🌴🏢🛡️ A ronda completa: Palmeira, Segunor e Protek de uma vez.",
            "Três NVRs ao mesmo tempo. O Zé está inspirado hoje.",
            "Liga tudo de uma vez. Eficiência máxima (de vigilância).",
            "A tartaruga fez o hat-trick: Palmeira, Segunor e Protek.",
        ]
    elif tem_palmeira and (tem_protek or tem_segunor):
        outro = "Protek" if tem_protek else "Segunor"
        frases = [
            f"🌴 A Palmeira e a {outro} ao mesmo tempo. Dois locais, uma tartaruga.",
            f"Palmeira + {outro}. O Zé não se cansa.",
            f"Liga à Palmeira e à {outro} de seguida. Ronda dupla confirmada.",
        ]
    elif tem_palmeira:
        frases = [
            "🌴 A Palmeira está a ser vigiada. O Zé chegou ao novo território.",
            "🌴 Acesso à Palmeira confirmado. As câmaras já sabem quem manda.",
            "🌴 O Zé entrou na Palmeira. Olhos em todo o lado.",
            "🌴 Palmeira no radar. A tartaruga expandiu o território.",
            "🌴 A Palmeira não escapou. Nenhum local escapa.",
            "🌴 Chegou à Palmeira. A vigilância não tem fronteiras.",
            "🌴 DVR da Palmeira acordou. O Zé está a olhar.",
            "🌴 A Palmeira sob escrutínio. As câmaras não mentem.",
            "🌴 O Zé visitou a Palmeira. Esperemos que seja de trabalho.",
            "🌴 Palmeira ativa. A tartaruga gosta de variedade.",
            "🌴 NVR Palmeira: online. Funcionários: em alerta.",
            "🌴 A Palmeira entrou no mapa da tartaruga. Bem-vindos ao clube.",
            "🌴 O Zé foi à Palmeira ver o que se passa. Como sempre.",
            "🌴 Acesso à Palmeira detetado. Alguém avisou os funcionários de lá?",
            "🌴 A tartaruga chegou à Palmeira. Ninguém está a salvo.",
        ]
    elif tem_protek:
        frases = [
            "🛡️ O clássico: acesso à Protek.",
            "🛡️ A Protek sob vigilância. Como de costume.",
        ]
    elif tem_segunor:
        frases = [
            "🏢 Acesso à Segunor. O Zé quer ver quem chegou tarde.",
            "🏢 Segunor em destaque hoje.",
        ]
    else:
        return ""

    return random.choice(frases)

def gerar_frase_saida():
    frases = [
        "A Tartaruga escondeu-se na carapaça (saiu).",
        "Foi comer alface (ou um bitoque).",
        "Mergulhou para longe do NVR.",
        "Desapareceu em combate (provavelmente foi lanchar).",
        "A Tartaruga Genial perdeu o rasto, provavelmente foi beber uma mini.",
        "Foi meter gasóleo no carapaça-móvel.",
        "Diz que vai ali e já vem (não vem).",
        "Abatido por excesso de trabalho ninja.",
        "Foi treinar o Kamehameha.",
        "Desligou-se para não ser apanhado pela Bulma.",
        "Saiu porque o cão apanhou outra galinha.",
        "Esta é a voz, vou cagar.",
        "Foi ver se chovia lá fora.",
        "O Mestre Roshi chamou-o para treinar (dormir).",
        "Foi procurar as Bolas de Cristal.",
        "Sessão terminada por excesso de estilo.",
        "Foi regar as plantas com imperiais.",
        "Fugiu antes que o trabalho o apanhasse.",
        "Foi ver se o Benfica ganhava.",
        "Diz que foi à pesca.",
        "A carapaça ficou apertada, foi trocar de roupa.",
        "Foi à procura do Mestre Splinter.",
        "Saiu para ir ver se o mar tem água.",
        "Desconectou-se para poupar os pixeis do monitor.",
        "Foi ver se o correio já chegou.",
        "Fingiu que trabalhou e bazou.",
        "Missão cumprida (ou talvez não).",
        "O sistema expulsou-o por excesso de carisma.",
        "Foi combater o crime noutro lado.",
        "A net caiu ou ele fugiu?",
        "Saiu para ir ao multibanco levantar milhões.",
        "Foi ver se a relva cresceu.",
        "Fartou-se de olhar para ecrãs.",
        "Foi ali e já vem (diz ele).",
        "Evaporou-se como ninja que é.",
        "Foi ver se o Sporting já foi campeão.",
        "Foi contar as ondas do mar.",
        "Foi procurar o D. Sebastião no nevoeiro.",
        "Foi ver se a máquina de café ainda funciona.",
        "Foi à casa de banho ler o jornal.",
        "Foi fazer fotossíntese lá para fora.",
        "Simulou uma falha técnica e fugiu.",
        "Foi ver se o carro ainda estava no parque.",
        "Foi comprar cigarros à espanha.",
        "Diz que foi a uma reunião importantíssima (na esplanada).",
        "A cadeira estava desconfortável, foi para o sofá.",
        "Foi ver se o sol ainda brilha.",
        "Saiu em modo fantasma antes que alguém lhe pedisse algo.",
    "Desapareceu misteriosamente quando cheirou trabalho.",
    "Foi ali… e nunca mais voltou.",
    "A carapaça entrou em modo avião.",
    "Saiu porque o café acabou.",
    "Evadiu-se como quem vai só à casa de banho.",
    "Foi dar uma volta para parecer ocupado.",
    "A tartaruga fechou a sessão sem avisar.",
    "Saiu para manter a média de esforço baixa.",
    "Desligou tudo e fingiu problemas técnicos.",
    "Foi treinar a arte milenar de não fazer nada.",
    "Saiu estrategicamente antes de alguém reparar.",
    "Desapareceu mais rápido do que entrou (milagre).",
    "Foi responder a uma chamada imaginária.",
    "A carapaça entrou em modo invisível.",
    "Saiu porque já tinha feito demais por hoje.",
    "Desconectou-se para preservar energia vital.",
    "Foi ali espairecer… há 3 horas.",
    "A tartaruga bateu em retirada tática.",
    "Saiu para não estragar a reputação.",
        "Saiu para não ter de aturar o Caçador."
    ]
    return random.choice(frases)

def gerar_frase_tedio():
    frases = [
        "A tartaruga adormeceu... 💤",
        "Que seca... A tartaruga deve estar com medo do caçador.",
        "O radar está a ganhar teias de aranha. Onde anda o Zé?",
        "Isto está mais parado que uma repartição de finanças.",
        "O Caçador está a polir a espingarda de tanto esperar.",
         "A tartaruga adormeceu... 💤",
        "A tartaruga ficou de pernas para o ar? Ninguém mexe há horas.",
        "Que seca... A tartaruga deve estar com medo do caçador.",
        "O radar está a ganhar teias de aranha. Onde anda o Zé?",
        "Será que a tartaruga virou sopa? 🍲",
        "Alguém viu uma carapaça por aí? Isto está muito calmo.",
        "O caçador está a ficar sem paciência...",
        "Zzz... Até o caçador está a adormecer à espera.",
        "Confirmado: A tartaruga é mesmo lenta. Zero movimento.",
        "Alguém que vá ver se o Zé ainda respira.",
        "Vou começar a contar azulejos se ninguém entrar em 5 minutos.",
        "Isto está mais parado que uma repartição de finanças.",
        "Câmbio? Tartaruga? Estás na escuta? Nada...",
        "O Caçador está a polir a espingarda de tanto esperar.",
        "Se isto fosse um filme de ação, o público já tinha pedido o reembolso.",
        "Ainda estou aqui... infelizmente.",
        "Alerta de tédio nível máximo. Enviem bifanas.",
        "O Zé deve ter ido de férias e não avisou o Caçador.",
        "Estou a pensar em mudar de profissão. Caçador de caracóis talvez seja mais emocionante.",
        "O NVR está a ganhar pó...",
        "Alguém quer jogar às cartas? Isto está parado.",
        "Sinto-me sozinho aqui...",
        "Eco... eco... eco... Ninguém?",
        "Vou começar a contar os pixeis da câmara 1.",
        "Acho que vi um fantasma passar. Ah não, foi só pó.",
        "Nada acontece… suspeito.",
    "Isto está tão parado que até o NVR boceja.",
    "A tartaruga deve ter entrado em modo decoração.",
    "Silêncio absoluto. Medo.",
    "O caçador já fez café, chá e sopa à espera.",
    "Nem uma sombra… dececionante.",
    "Confirmado: zero movimento, zero emoção.",
    "Se isto fosse uma série, já tinha sido cancelada.",
    "A câmara continua ligada por teimosia.",
    "Nada mexe. A vida é dura.",
    "Até o pó está imóvel.",
    "O Zé deve estar a praticar invisibilidade.",
    "Começo a suspeitar que ele virou planta.",
    "Estou a vigiar… mas vigiar o quê?",
    "Mais parado que trânsito às 3 da manhã.",
        "Tanto silêncio que até oiço os cabos a zumbir.",
                "Nada a reportar. O Zé deve estar a fingir que trabalha.",
        "Silêncio total. Suspeito.",
        "Isto está tão parado que até o NVR boceja.",
        "Nenhum movimento. A carapaça deve estar a dormir.",
        "O caçador continua à espera… infelizmente.",
        "Zero atividade. Almoços longos devem estar a acontecer fora do radar.",
        "O radar está ligado, mas a paciência está no fim.",
        "Nada mexe. Nem a dignidade.",
        "Confirmado: o Zé não está a cuscar. Milagre.",
        "Câmaras ligadas, esperança desligada.",
        "Isto está mais morto que a motivação à segunda-feira.",
        "O NVR está a servir de decoração.",
        "Silêncio absoluto. Alguém deve estar feliz.",
        "Nada no CCTV. A equipa respira.",
        "O caçador está a considerar ir dormir mais cedo.",
        "Zero entradas. Zero stress. Zero emoção.",
        "Nem um clique. Que desilusão.",
        "O sistema está ativo, a ação não.",
        "Estou a vigiar… o vazio.",
        "Nada acontece há tanto tempo que já dói.",
        "Isto está parado que nem fila do multibanco às 3 da manhã.",
        "A carapaça deve estar em reunião imaginária.",
        "Nenhum acesso ao DVR. Estranho.",
        "O caçador já fez café. Dois.",
        "Silêncio suficiente para ouvir pensamentos perigosos.",
        "Nada a assinalar. A suspeita mantém-se.",
        "O radar não apanhou nada além de tédio.",
        "Câmaras on. Vida off.",
        "Isto está a testar a sanidade do caçador.",
        "Nenhum movimento suspeito. Só desilusão.",
        "O Zé deve estar a fazer de conta que não existe CCTV.",
        "Mais parado que um almoço de equipa sem sobremesa.",
        "Nada mexe. Nem o rato.",
        "A vigilância continua. A emoção morreu.",
        "O caçador começa a questionar as escolhas de vida.",
        "Nenhum login. Nenhuma cusquice.",
        "O NVR está ligado por teimosia.",
        "Isto parece um feriado não anunciado.",
        "Silêncio no sistema. Barulho na cabeça.",
        "Nada acontece. E eu continuo aqui.",
        "A carapaça deve estar em modo offline existencial.",
        "Mais um minuto de pura seca.",
        "Câmaras vazias. Expectativas também.",
        "Isto está tão calmo que assusta.",
        "Zero atividade. O Zé deve estar ocupado a dar opiniões inúteis.",
        "Nenhuma entrada no DVR. Milagre ou armadilha?",
        "O caçador está a ganhar raízes.",
        "Nada a reportar. De novo.",
        "O sistema está ligado só para fazer número.",
        "Este silêncio não é natural.",
        "Nada mexe. Nem o relógio parece andar.",
        "O Zé deve estar a planear algo. Ou não.",
        "A vigilância continua. O entusiasmo não.",
        "Câmaras ligadas. Nada para ver.",
        "Estou a vigiar o nada em alta definição.",
        "Nenhum acesso. Nenhuma razão para existir.",
        "Isto está a ficar pessoal.",
        "Nada acontece e eu continuo pago para olhar.",
        "O NVR está a apanhar pó digital.",
        "Zero eventos. Zero drama.",
        "Silêncio total. A equipa agradece.",
        "O caçador está a contar minutos como quem conta dias.",
        "Nada mexe. Alguém deve estar feliz com isso.",
        "Isto está mais parado que promessa de aumento.",
        "Nenhum sinal do Zé. Suspeito.",
        "A carapaça deve estar em modo avião.",
        "Nada a ver. Nada a dizer.",
        "O sistema continua ligado só por pirraça.",
        "O tédio já devia pagar renda.",
        "Nada acontece. E eu sigo firme.",
        "Câmaras vazias. Alma vazia.",
        "Se isto fosse um filme, já tinha acabado.",
        "O caçador mantém-se alerta… contra o nada."
    ]
    return random.choice(frases)

def formatar_tempo(segundos):
    """Converte segundos em formato legível (ex: 1h 5min, 10min 32s)."""
    if segundos < 60:
        return f"{int(segundos)}s"
    elif segundos < 3600:
        mins = int(segundos // 60)
        secs = int(segundos % 60)
        return f"{mins}min {secs}s" if secs > 0 else f"{mins}min"
    else:
        horas = int(segundos // 3600)
        mins = int((segundos % 3600) // 60)
        return f"{horas}h {mins}min" if mins > 0 else f"{horas}h"

def gerar_frase_resumo_dia(protek, segunor, palmeira=0, tempo_protek=0, tempo_segunor=0, tempo_palmeira=0):
    total = protek + segunor + palmeira
    tempo_total = tempo_protek + tempo_segunor + tempo_palmeira

    # Formatar tempos
    tp = formatar_tempo(tempo_protek)
    ts_str = formatar_tempo(tempo_segunor)
    tpal = formatar_tempo(tempo_palmeira)
    tt = formatar_tempo(tempo_total)

    templates = [
        f"Hoje o caçador apanhou a tartaruga {total} vezes ({protek} Protek / {segunor} Segunor / {palmeira} Palmeira).",
        f"Relatório de caça: {total} deteções ({protek} P + {segunor} S + {palmeira} Palm).",
        f"A Tartaruga foi detetada {total} vezes no total.",
        f"Score final: Caçador {total} - 0 Tartaruga.",
        f"Estatísticas: {protek} na Protek, {segunor} na Segunor, {palmeira} na Palmeira. Total: {total}.",
        f"Hoje o caçador apanhou a tartaruga com as calças na mão {protek} vezes na Protek, {segunor} na Segunor e {palmeira} na Palmeira.",
        f"Relatório de caça: Fiz sopa de tartaruga {protek} vezes na Protek, {segunor} na Segunor e {palmeira} na Palmeira.",
        f"A Tartaruga Genial tentou ser ninja, mas foi detetada {total} vezes no total ({protek} P / {segunor} S / {palmeira} Palm).",
        f"Resumo da novela: A Tartaruga apareceu {protek} vezes na Protek, {segunor} na Segunor e {palmeira} na Palmeira. Amanhã há mais.",
        f"O radar disparou {total} vezes hoje. {protek} tiros na Protek, {segunor} na Segunor e {palmeira} na Palmeira.",
        f"Apanhada! {protek} vezes na Protek, {segunor} na Segunor e {palmeira} na Palmeira. Sem escapatória.",
        f"Dia fraco (ou forte?): {total} aparições. {protek} na Protek, {segunor} na Segunor, {palmeira} na Palmeira.",
        f"Estatísticas da carapaça: {total} movimentos detetados ({protek} Protek, {segunor} Segunor, {palmeira} Palmeira).",
        f"O Zé hoje trabalhou... ou pelo menos fingiu {total} vezes.",
        f"Fim do dia. Score: Caçador {total} - 0 Tartaruga.",
        f"Balanço final: A tartaruga espreitou {total} vezes. {protek} Protek, {segunor} Segunor, {palmeira} Palmeira.",
        f"Dia produtivo para o Caçador: {total} avistamentos confirmados.",
        f"A Tartaruga andou elétrica hoje: {total} logins. Deve ter bebido café a mais.",
        f"Resumo rápido: {protek} Protek, {segunor} Segunor, {palmeira} Palmeira. Total: {total}. Agora cama.",
        f"Contagem oficial: O Zé picou o ponto {total} vezes. {protek}P + {segunor}S + {palmeira}Palm.",
        f"Resumo do dia: o Zé esteve a cuscar {total} vezes — {protek} na Protek, {segunor} na Segunor e {palmeira} na Palmeira. Trabalhar? Isso é opcional.",
        f"Hoje o fiscal de carapaça entrou no DVR {total} vezes ({protek} Protek / {segunor} Segunor / {palmeira} Palmeira). Almoços bem analisados.",
        f"Relatório interno: {total} acessos do Zé ao CCTV. {protek} para vigiar, {segunor} para desconfiar, {palmeira} para confirmar.",
        f"O Zé passou o dia a ver câmaras: {protek} visitas à Protek, {segunor} à Segunor e {palmeira} à Palmeira. Reality show completo.",
        f"Resumo da bisbilhotice: {total} entradas no sistema. Funcionários observados, carapaça satisfeita.",
        f"Hoje foi dia de Big Brother: {total} sessões de CCTV ({protek} P / {segunor} S / {palmeira} Palm).",
        f"O DVR não teve descanso: {total} acessos do Zé só para ver quem estava parado.",
        f"Relatório final: {total} vezes a controlar horários e almoços. {protek} Protek, {segunor} Segunor, {palmeira} Palmeira.",
        f"O Zé ligou-se {total} vezes hoje. Produção zero, controlo máximo.",
        f"Dia intenso no CCTV: {protek} Protek, {segunor} Segunor, {palmeira} Palmeira. O olho que tudo vê.",
        f"Resumo oficial: {total} acessos ao DVR para ver quem chegou tarde e saiu cedo.",
        f"Hoje o Zé fez de chefe remoto {total} vezes ({protek} Protek / {segunor} Segunor / {palmeira} Palmeira).",
        f"O fiscal entrou no sistema {total} vezes. Funcionários nervosos, Zé entretido.",
        f"Relatório do Caçador: {total} rondas digitais do Zé às câmaras.",
        f"Resumo do controlo: {total} acessos ao CCTV. Almoços cronometrados.",
        f"Hoje o Zé esteve em modo stalker {total} vezes. {protek} P, {segunor} S, {palmeira} Palm.",
        f"O DVR foi mais usado que o Excel: {total} acessos do Zé. {protek} P, {segunor} S, {palmeira} Palm.",
        f"Resumo do dia: muita câmara, pouco trabalho. {total} acessos registados. {protek} P, {segunor} S, {palmeira} Palm.",
        f"Hoje o Zé fiscalizou a malta {total} vezes através das câmaras. {protek} P, {segunor} S, {palmeira} Palm.",
        f"Dia fechado: {total} entradas no CCTV só para ver quem estava a fazer nenhum.",
        f"Três NVRs, uma tartaruga: {protek} na Protek, {segunor} na Segunor e {palmeira} na Palmeira. O Zé não para.",
        f"A Palmeira também não escapou: {palmeira} visitas lá, {protek} na Protek e {segunor} na Segunor. Total: {total}.",
        f"Hoje o Zé fez a ronda completa: Protek ({protek}), Segunor ({segunor}), Palmeira ({palmeira}). Nada lhe escapa.",
        f"O CCTV da Palmeira já sabe quem manda: {palmeira} visitas hoje. Mais {protek} na Protek e {segunor} na Segunor.",
        f"Deteções hoje: {protek} Protek + {segunor} Segunor + {palmeira} Palmeira = {total} vezes apanhado.",
        f"A tartaruga expandiu o território: {total} acessos espalhados pelos três NVRs.",
        f"Relatório de expansão: a Palmeira entrou no mapa com {palmeira} visitas. Protek teve {protek}, Segunor {segunor}.",
        f"O Zé tem três DVRs e usou-os todos: {protek} P, {segunor} S, {palmeira} Palm. Dia completo.",
        f"Vigilância total: {total} sessões hoje. A Palmeira contribuiu com {palmeira}.",
        f"Resumo da tríade: Protek {protek} | Segunor {segunor} | Palmeira {palmeira} | Total {total}.",
    ]

    frase = random.choice(templates)

    # Adicionar tempo total se houve atividade
    if tempo_total > 0:
        frase += f"\n⏱️ Tempo total a cuscar: {tt} ({tp} Protek / {ts_str} Segunor / {tpal} Palmeira)"

    # Duração de playback
    if tempo_playback_total > 0:
        frase += f"\n📼 Tempo em playback: {formatar_tempo(tempo_playback_total)}"

    # Heatmap horário
    heatmap = gerar_heatmap_texto()
    if heatmap:
        frase += f"\n{heatmap}"

    # Adicionar comparação com média semanal
    comparacao = gerar_comparacao_media(protek, segunor, tempo_protek, tempo_segunor, palmeira, tempo_palmeira)
    if comparacao:
        frase += f"\n\n{comparacao}"

    return frase

def gerar_frase_fim_turno():
    frases = [
        "O turno do caçador acabou por hoje.",
        "Até amanhã, que o caçador também precisa de dormir.",
        "Fim da caçada diária. Câmbio e desligo.",
        "Game Over. Insert coin tomorrow.",
        "O turno do caçador acabou por hoje.",
        "O caçador vai descansar a espingarda.",
        "O caçador vai terminar o turno, bom trabalho a todos (ou boa noite).",
        "Até amanhã, que o caçador também precisa de dormir.",
        "Fim da caçada diária. Câmbio e desligo.",
        "A loja fecha agora. O caçador vai beber um copo.",
        "Acabou-se a festa. O Caçador vai ver televisão.",
        "Vou dormir, não façam barulho.",
        "Game Over. Insert coin tomorrow.",
        "A Tartaruga venceu por hoje (porque eu deixei).",
        "Encerrado para balanço de sarcasmo.",
        "O último a sair que apague a luz.",
        "Fui. Amanhã há mais perseguição.",
        "O Caçador retira-se para os seus aposentos.",
        "Vou recarregar as baterias de ironia.",
        "O caçador vai descansar os olhos (e a paciência).",
    "Fim do turno. A tartaruga que durma descansada.",
    "Encerrado. Amanhã há mais humilhação.",
    "O caçador desliga antes de enlouquecer.",
    "Missão terminada. Sanidade em risco.",
    "Fim da vigilância. A ironia fica ligada.",
    "Vou dormir antes que ele apareça outra vez.",
    "Sistema desligado por excesso de tédio.",
    "Fim de turno. O sarcasmo volta amanhã.",
    "Desligar antes que alguém mexa.",
           "O caçador vai dormir. Se o Zé entrar agora, azar.",
        "Desligo os alertas e vou fingir que não existe CCTV.",
        "Fim de turno. Agora que se vigiem uns aos outros.",
        "O caçador bazou. O Zé que cusque sozinho.",
        "Vou dormir antes que ele se lembre de entrar no DVR.",
        "Alertas off. Sanidade on (ou o que resta).",
        "O caçador fecha os olhos. Aproveitem para respirar.",
        "Fim da vigilância. Quem mexer agora passa entre pingos.",
        "Desligado. O Big Brother fica para amanhã.",
        "Vou dormir. Se o Zé entrar, ninguém viu nada.",
        "Caçador offline. Hora oficial de fazer nenhum.",
        "Alertas encerrados. Agora é terra sem lei.",
        "O caçador recolhe. A carapaça que espere por amanhã.",
        "Fim do turno. A equipa agradece.",
        "Vou descansar antes que alguém seja apanhado outra vez.",
        "Câmbio e desligo. Amanhã voltamos ao circo.",
        "O caçador saiu. Aproveitem a liberdade condicional.",
        "Desligado por excesso de cusquice alheia.",
        "Fim da emissão. Pipocas amanhã.",
        "O caçador foi dormir. CCTV sem supervisão humana.",
        "Vou desligar isto antes de perder a fé na humanidade.",
        "Caçador fora. Agora é modo sobrevivência.",
        "Turno fechado. O sarcasmo dorme comigo.",
        "Vou dormir. Se ouvirem alertas, foi imaginação.",
        "Desligar tudo e fingir que trabalho remoto existe."
    ]
    return random.choice(frases)

def gerar_frase_arranque():
    frases = [
        "🐢 *Caçador de Tartarugas V15.00 AO SERVIÇO*",
        "🐢 *Sistema de Vigilância V15.00: INICIADO*",
        "🐢 *Radar de Tartarugas V15.00: ON AIR*",
        "🐢 *Caçador Acordou: CUIDADO ZÉ*",
        "🐢 *Caçador de Tartarugas V15.00 AO SERVIÇO*",
        "🐢 *Sistema de Vigilância V15.00: INICIADO*",
        "🐢 *Radar de Tartarugas V15.00: ON AIR*",
        "🐢 *Protocolo Caça-Tartaruga V15.00: ATIVO*",
        "🐢 *Olhos na Carapaça V15.00: ONLINE*",
        "🐢 *A Caçada Começou! V15.00 ONLINE*",
        "🐢 *Caçador Acordou: CUIDADO ZÉ*",
        "🐢 *Sistema Pronto para Apanhar Tartarugas*",
        "🐢 *Vigilância Ativa: O Zé que se cuide*",
        "🐢 *Caçador Online: Escondam as carapaças*",
        "🐢 *Sistema Ativo. Zé, estás lixado.*",
    "🐢 *Radar ligado. Paciência desligada.*",
    "🐢 *Caçador online. Boa sorte, tartaruga.*",
    "🐢 *Modo perseguição passiva ativado.*",
    "🐢 *Vigilância iniciada. Fingir que não vês não resulta.*",
    "🐢 *Sistema armado. Ironia carregada.*",
    "🐢 *Olhos abertos. Café tomado.*",
    "🐢 *Pronto para detetar movimentos mínimos.*"
    ]
    return random.choice(frases)

def gerar_frase_bom_dia():
    frases = [
        "☀️ *BOM DIA, CAÇADOR DE TARTARUGAS*",
        "☀️ *ACORDA CAÇADOR! O TURNO COMEÇOU*",
        "☀️ *OLHOS ABERTOS: A CAÇADA RECOMEÇA*"
         "☀️ *BOM DIA, CAÇADOR DE TARTARUGAS*",
        "☀️ *ACORDA CAÇADOR! O TURNO COMEÇOU*",
        "☀️ *JÁ CANTA O GALO (E A TARTARUGA FOGE)*",
        "☀️ *SISTEMAS REINICIADOS. CAFÉ TOMADO.*",
        "☀️ *OLHOS ABERTOS: A CAÇADA RECOMEÇA*",
        "☀️ *BIP BIP. MODO DIURNO ATIVADO.*",
        "☀️ *O SOL NASCEU E O RADAR TAMBÉM.*",
        "☀️ *QUEM É QUE VAMOS APANHAR HOJE?*",
        "☀️ *READY TO RUMBLE: DIA DE CAÇA.*",
        "☀️ *A TARTARUGA ACHAVA QUE SE LIVRAVA? ENGANOU-SE.*",
        "☀️ *BOM DIA, MALTA. ESCONDAM OS CAFÉS E OS ALMOÇOS LONGOS*",
        "☀️ *ACORDEM. O FISCAL DE CARAPAÇA JÁ DEVE ESTAR ONLINE*",
        "☀️ *NOVO DIA, MESMO ZÉ A CUSCAR*",
        "☀️ *CAFÉ NA MÃO, OLHO NO DVR*",
        "☀️ *BOM DIA! TRABALHAR É OPCIONAL, VIGIAR NÃO*",
        "☀️ *O SOL NASCEU E O ZÉ LIGOU O CCTV*",
        "☀️ *DIA NOVO. PAUSAS CURTAS. CARAPAÇA ATENTA*",
        "☀️ *LEVANTEM-SE QUE O BIG BROTHER ACORDOU*",
        "☀️ *BOM DIA. FINJAM QUE ESTÃO OCUPADOS*",
        "☀️ *A CAÇADA COMEÇA. ESCONDAM OS TUPPERWARES*"
    ]
    return random.choice(frases)

def gerar_frase_playback():
    frases = [
        "A ver gravações antigas 📼",
        "Em modo CSI - a investigar o passado 🔍",
        "A rebobinar a fita (playback) ⏪",
        "Em modo detetive - gravações em análise 🎞️",
        "A espreitar o que aconteceu antes 🕵️",
        "A ver gravações antigas 📼",
        "Em modo CSI - a investigar o passado 🔍",
        "A rebobinar a fita (playback) ⏪",
        "A ver o que aconteceu antes 🕵️",
        "Em modo detetive - gravações em análise 🎞️",
                "A rever gravações antigas 📼 (para rir depois)",
        "Modo CSI: Onde é que ele fingiu trabalhar? 🔍",
        "A rebobinar para apanhar a mentira ⏪",
        "Playback ativo: prova irrefutável da preguiça",
        "A ver o passado… cheio de nada 🕵️",
        "Análise forense da carapaça 🎞️",
        "A rever momentos em que devia estar a trabalhar",
        "Replay em câmara lenta da falta de esforço",
        "A procurar produtividade nas gravações (spoiler: não há)",
        "Modo detetive: à caça de um minuto útil",
        "A confirmar que não foi bug, foi mesmo o Zé",
        "A rever merda antiga para justificar a vigilância",
                "Playback ativo 📼 à procura de almoços longos",
        "A rever gravações para mandar bocas depois",
        "Modo CSI: quem saiu cedo? 🔍",
        "Playback ligado. A culpa vai aparecer.",
        "A rebobinar para apanhar alguém parado ⏪",
        "Análise forense de pausas suspeitas",
        "A rever o passado para estragar o presente",
        "Playback ativo: provas em construção",
        "À procura daquele minuto de preguiça",
        "Modo detetive corporativo ligado",
        "A confirmar que não foi imaginação",
        "Rever gravações para justificar o stress"
        # ... adiciona mais frases aqui
    ]
    return random.choice(frases)


# ==========================================
#  FUNÇÕES AUXILIARES
# ==========================================

def is_within_schedule():
    now_dt = datetime.datetime.now()
    if now_dt.weekday() >= 5:
        return False 
    now_time = now_dt.time()
    return HORA_INICIO <= now_time <= HORA_FIM

def is_friday_end_of_day():
    now_dt = datetime.datetime.now()
    return now_dt.weekday() == 4

def enviar_notificacao_telegram(mensagem, force=False):
    global missed_access_count_night
    
    if not is_within_schedule() and not force:
        if "DETETADO" in mensagem.upper() or "ALVO" in mensagem.upper(): 
            missed_access_count_night += 1
            ts = datetime.datetime.now().strftime('%H:%M:%S')
            print(f"[{ts}] 🌙 [SILÊNCIO] Acesso contado (Total: {missed_access_count_night}).")
        return True
        
    url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
    payload = {"chat_id": TELEGRAM_CHAT_ID, "text": mensagem, "parse_mode": "Markdown"}
    
    for tentativa in range(3):
        try:
            resp = requests.post(url, json=payload, timeout=10)
            if resp.status_code == 200:
                return True
            time.sleep(1)
        except Exception as e:
            ts = datetime.datetime.now().strftime('%H:%M:%S')
            print(f"[{ts}] [!] Telegram erro (tentativa {tentativa+1}): {e}")
            time.sleep(2)
    
    return False

def check_nvr_online(gravador):
    url = f"http://{gravador['ip']}/cgi-bin/userManager.cgi?action=getActiveUserInfoAll"
    try:
        res = requests.get(url, auth=HTTPDigestAuth(gravador['user'], gravador['pass']), timeout=5)
        return res.status_code == 200
    except:
        return False

def extrair_valor(bloco, campo):
    # Tenta vários padrões possíveis
    patterns = [
        f"{campo}=([^;\r\n]+)",
        f"{campo.lower()}=([^;\r\n]+)",
        f"{campo.upper()}=([^;\r\n]+)"
    ]
    for pattern in patterns:
        match = re.search(pattern, bloco, re.IGNORECASE)
        if match:
            return match.group(1).strip()
    return ""

def sondagem_ativa(gravador):
    """Obtém utilizadores ativos com informação detalhada."""
    url = f"http://{gravador['ip']}/cgi-bin/userManager.cgi?action=getActiveUserInfoAll"
    try:
        res = requests.get(url, auth=HTTPDigestAuth(gravador['user'], gravador['pass']), timeout=5)
        if res.status_code == 200:
            users_map = {}
            for linha in res.text.splitlines():
                match_idx = re.search(r'\[(\d+)\]', linha)
                if match_idx:
                    idx = match_idx.group(1)
                    users_map[idx] = users_map.get(idx, "") + linha + "\n"
            
            nvr_nome = gravador.get('nome', '')
            alvos_nvr = [u.lower() for u in USERS_ALVO_POR_NVR.get(nvr_nome, USERS_ALVO)]

            encontrados = []
            for idx in users_map:
                bloco = users_map[idx]
                nome = extrair_valor(bloco, "Name")
                # Logging defensivo Palmeira: capturar bloco completo de cada sessão (uma vez por Id)
                if nvr_nome == "NVR Palmeira" and bloco.strip():
                    sess_id = extrair_valor(bloco, "Id")
                    nome_sess = extrair_valor(bloco, "Name")
                    if nome_sess.lower() != "bot" and sess_id and sess_id not in _palmeira_logged_ids:
                        _palmeira_logged_ids.add(sess_id)
                        bloco_compacto = " | ".join(l.strip() for l in bloco.splitlines() if l.strip())
                        print(f"[PALMEIRA-SESSAO?] {bloco_compacto[:600]}")

                if nome.lower() not in alvos_nvr:
                    continue

                ip = extrair_valor(bloco, "Address") or extrair_valor(bloco, "IP")
                login_time = extrair_valor(bloco, "LoginTime")
                client_type = extrair_valor(bloco, "ClientType") or extrair_valor(bloco, "Type")
                group = extrair_valor(bloco, "Group")

                # Filtro adicional para admin no Palmeira: só DMSS cloud share.
                # Critério: IP loopback (127.0.0.x) é o sinal fiável — o NVR usa loopback interno
                # para representar sessões vindas do canal P2P/cloud share.
                # ClientType varia (DVRIP, Local, Dahua3.0) e não é discriminante por si só.
                # Também exclui sessões pré-arranque do script (admin persistentes antigos).
                if nvr_nome == "NVR Palmeira" and nome.lower() == "admin":
                    if not ip.startswith("127.0.0."):
                        print(f"[PALMEIRA-FILTRO] admin ignorado (IP='{ip}' não é loopback, prov. local/manutenção)")
                        continue
                    try:
                        lt = datetime.datetime.strptime(login_time, '%Y-%m-%d %H:%M:%S')
                        if lt < SCRIPT_START_TIME:
                            # Sessão persistente pré-arranque — Id antigo, não conta
                            continue
                    except (ValueError, TypeError):
                        print(f"[PALMEIRA-FILTRO] admin ignorado (LoginTime inválido: '{login_time}')")
                        continue
                    # Print apenas uma vez por sessão (LoginTime único) para evitar inundar logs a cada poll
                    sess_id = extrair_valor(bloco, "Id")
                    match_key = f"match:{login_time}:{sess_id}"
                    if match_key not in _palmeira_logged_ids:
                        _palmeira_logged_ids.add(match_key)
                        print(f"[PALMEIRA-MATCH] admin via DMSS detetado: IP={ip} ClientType={client_type} LoginTime={login_time} Id={sess_id}")

                encontrados.append({
                    "user": nome,
                    "ip": ip,
                    "login_time": login_time,
                    "client_type": client_type,
                    "group": group
                })
            return encontrados
        return None
    except Exception as e:
        return None


# ==========================================
#  THREADS DE GESTÃO
# ==========================================

def thread_schedule_manager():
    """Gere horário, relatórios e modo tédio."""
    global sleep_mode_active, missed_access_count_night, daily_stats, last_activity_time, tempo_playback_total
    sleep_mode_active = not is_within_schedule()
    
    while True:
        try:
            time.sleep(60)
            ts = datetime.datetime.now().strftime('%H:%M:%S')
            now_dt = datetime.datetime.now()
            
            # Check NVRs a cada hora
            if now_dt.minute == 0:
                for g in GRAVADORES:
                    is_online = check_nvr_online(g)
                    nome = g['nome']
                    
                    if is_online != nvr_status_cache.get(nome, True):
                        if not is_online:
                            print(f"[{ts}] [NET FAIL] {nome} OFFLINE.")
                            enviar_notificacao_telegram(f"🚨 *ALERTA:* O gravador `{nome}` ficou OFFLINE!", force=True)
                        else:
                            print(f"[{ts}] [NET OK] {nome} ONLINE.")
                            enviar_notificacao_telegram(f"✅ *RECUPERADO:* O gravador `{nome}` voltou online.", force=True)
                        nvr_status_cache[nome] = is_online

            within = is_within_schedule()

            # TRANSIÇÃO: Ativo -> Sleep (fim do turno)
            if not within and not sleep_mode_active:
                sleep_mode_active = True
                
                protek_count = daily_stats.get("NVR Protek", 0)
                segunor_count = daily_stats.get("NVR Segunor", 0)
                palmeira_count = daily_stats.get("NVR Palmeira", 0)

                # Acumular tempo de sessões ainda ativas no fim do dia
                with lock:
                    for chave, dados in sessoes_ativas.items():
                        if dados.get('confirmed'):
                            duracao = time.time() - dados['first_seen']
                            nvr = dados['nvr_nome']
                            daily_time[nvr] = daily_time.get(nvr, 0.0) + duracao
                            dados['first_seen'] = time.time()

                    # Flush de playback ainda em curso (entre o último tick e agora)
                    agora_flush = time.time()
                    for u in list(playback_start_time.keys()):
                        delta = agora_flush - playback_start_time[u]
                        if delta > 0:
                            tempo_playback_total += delta
                            playback_start_time[u] = agora_flush

                tempo_protek = daily_time.get("NVR Protek", 0)
                tempo_segunor = daily_time.get("NVR Segunor", 0)
                tempo_palmeira = daily_time.get("NVR Palmeira", 0)
                frase_stats = gerar_frase_resumo_dia(protek_count, segunor_count, palmeira_count, tempo_protek, tempo_segunor, tempo_palmeira)
                
                # Atualizar recordes diários
                if atualizar_recordes_diarios():
                    frase_stats += f"\n\n🏆 *NOVO RECORDE DIÁRIO!*"
                
                if is_friday_end_of_day():
                    msg = f"🌙 *MODO FIM DE SEMANA*\n\n📊 {frase_stats}\n\nO Caçador foi de folga. Até Segunda-Feira!"
                else:
                    frase_fim = gerar_frase_fim_turno()
                    msg = f"🌙 *{frase_fim}*\n\n📊 {frase_stats}\n\nNotificações suspensas até amanhã."
                
                print(f"[{ts}] [*] Sleep Mode ON. Stats: P={protek_count}, S={segunor_count}")
                enviar_notificacao_telegram(msg, force=True)
                
                # Relatório semanal à sexta-feira
                if is_friday_end_of_day():
                    relatorio = gerar_relatorio_semanal()
                    enviar_notificacao_telegram(relatorio, force=True)
                    print(f"[{ts}] 📊 Relatório semanal enviado")
                
                # Guardar no histórico antes de resetar
                today_str = datetime.datetime.now().strftime('%Y-%m-%d')
                history.append({
                    'date': today_str,
                    'stats': dict(daily_stats),
                    'time': dict(daily_time)
                })
                while len(history) > HISTORY_MAX_DAYS:
                    history.pop(0)
                
                # Reset estatísticas diárias (canais NÃO resetam — são cumulativos)
                for nome in daily_stats:
                    daily_stats[nome] = 0
                for nome in daily_time:
                    daily_time[nome] = 0.0
                acessos_por_hora.clear()
                tempo_playback_total = 0.0
                playback_start_time.clear()
                
                guardar_dados()

            # TRANSIÇÃO: Sleep -> Ativo (início do turno)
            elif within and sleep_mode_active:
                sleep_mode_active = False
                last_activity_time = time.time()
                
                msg_bom_dia = gerar_frase_bom_dia()
                msg = f"{msg_bom_dia}\n\nO turno de vigilância começou."
                
                if missed_access_count_night > 0:
                    msg += f"\n\n📊 *Relatório noturno:* A Tartaruga entrou `{missed_access_count_night}` vezes durante o silêncio."
                else:
                    msg += "\n\n✅ Nenhuma atividade registada durante o descanso."
                
                print(f"[{ts}] [*] Sleep Mode OFF.")
                enviar_notificacao_telegram(msg, force=True)
                missed_access_count_night = 0

            # MODO TÉDIO
            if within and (time.time() - last_activity_time > TEMPO_TEDIO):
                frase_tedio = gerar_frase_tedio()
                print(f"[{ts}] 💤 Tédio: {frase_tedio}")
                enviar_notificacao_telegram(f"🥱 *STATUS UPDATE*\n\n_{frase_tedio}_")
                last_activity_time = time.time()

        except Exception as e:
            print(f"[!] Erro schedule_manager: {e}")


def thread_session_validator():
    """
    Valida sessões pendentes.
    Só confirma login se a sessão persistir mais que TEMPO_MINIMO_SESSAO_REAL.
    Filtra handshakes DMSS que duram apenas 1-3 segundos.
    ANTI-MULTI-NVR: Se o mesmo user entra em múltiplos NVRs ao mesmo tempo,
    aumenta o threshold para TEMPO_MINIMO_MULTI_NVR (30s).
    """
    while True:
        try:
            time.sleep(0.5)
            agora = time.time()
            to_confirm = []
            to_discard = []
            
            with lock:
                # Agrupar sessões pendentes por user para detetar multi-NVR
                user_pending_nvrs = {}  # {user: [(chave, info, nvr_nome), ...]}
                for chave, info in list(sessoes_pendentes.items()):
                    user = info['user']
                    nvr = info['nvr_nome']
                    if user not in user_pending_nvrs:
                        user_pending_nvrs[user] = []
                    user_pending_nvrs[user].append((chave, info, nvr))
                
                for chave, info in list(sessoes_pendentes.items()):
                    tempo_ativo = agora - info['first_seen']
                    user = info['user']
                    
                    # Determinar threshold: verificar se é multi-NVR simultâneo
                    threshold = TEMPO_MINIMO_SESSAO_REAL
                    
                    if user in user_pending_nvrs:
                        # Contar NVRs DIFERENTES com sessões pendentes do mesmo user
                        nvrs_pendentes = set(nvr for _, _, nvr in user_pending_nvrs[user])
                        
                        if len(nvrs_pendentes) > 1:
                            # Verificar se entraram quase ao mesmo tempo (< 5s entre eles)
                            tempos = [i['first_seen'] for _, i, _ in user_pending_nvrs[user]]
                            spread = max(tempos) - min(tempos)
                            
                            if spread < 5.0:
                                # Multi-NVR simultâneo detetado → usar threshold maior
                                threshold = TEMPO_MINIMO_MULTI_NVR
                                if tempo_ativo < threshold and tempo_ativo > TEMPO_MINIMO_SESSAO_REAL:
                                    ts = datetime.datetime.now().strftime('%H:%M:%S')
                                    # Log apenas uma vez (a cada ~10s)
                                    if int(tempo_ativo) % 10 == 0:
                                        print(f"[{ts}] ⏳ Multi-NVR: {user} em {len(nvrs_pendentes)} NVRs, aguardando {threshold}s ({tempo_ativo:.0f}s)")
                    
                    # Sessão ainda existe e passou tempo mínimo → CONFIRMAR
                    if chave in sessoes_ativas and tempo_ativo >= threshold:
                        to_confirm.append((chave, info))
                    
                    # Sessão desapareceu antes do tempo mínimo → HANDSHAKE (ignorar)
                    elif chave not in sessoes_ativas:
                        to_discard.append((chave, info, tempo_ativo))
                    
                    # Timeout de segurança (60s sem confirmar = algo errado)
                    elif tempo_ativo > 60:
                        to_discard.append((chave, info, tempo_ativo))
            
            # Processar confirmações
            for chave, info in to_confirm:
                with lock:
                    if chave in sessoes_pendentes:
                        del sessoes_pendentes[chave]
                    
                    if chave in sessoes_ativas:
                        sessoes_ativas[chave]['confirmed'] = True
                        
                        user_name = info['user']
                        nvr_nome = info['nvr_nome']
                        
                        ts = datetime.datetime.now().strftime('%H:%M:%S')
                        print(f"[{ts}] ✅ CONFIRMADO: {user_name} @ {nvr_nome}")
                        
                        # Incrementar estatísticas
                        if is_within_schedule():
                            daily_stats[nvr_nome] = daily_stats.get(nvr_nome, 0) + 1
                            # Heatmap: registar hora do acesso
                            hora_atual = datetime.datetime.now().strftime('%H')
                            acessos_por_hora[hora_atual] = acessos_por_hora.get(hora_atual, 0) + 1
                        
                        # Adicionar ao buffer de login
                        if user_name not in login_buffer:
                            login_buffer[user_name] = {"nvrs": set(), "first_time": time.time()}
                        login_buffer[user_name]["nvrs"].add(nvr_nome)
            
            # Processar descartes (handshakes)
            for chave, info, tempo in to_discard:
                with lock:
                    if chave in sessoes_pendentes:
                        del sessoes_pendentes[chave]
                ts = datetime.datetime.now().strftime('%H:%M:%S')
                print(f"[{ts}] 🔄 Handshake ignorado ({tempo:.1f}s): {info['user']} @ {info['nvr_nome']}")
                        
        except Exception as e:
            ts = datetime.datetime.now().strftime('%H:%M:%S')
            print(f"[{ts}] [!] Erro session_validator: {e}")


def thread_login_manager():
    """Processa logins CONFIRMADOS agrupados."""
    global last_activity_time, anomalia_hora_last_alert
    
    while True:
        try:
            time.sleep(0.5)
            agora = time.time()
            to_send = []
            
            with lock:
                for user, info in list(login_buffer.items()):
                    if agora - info['first_time'] > TEMPO_AGRUPAMENTO:
                        to_send.append((user, list(info['nvrs'])))
                        del login_buffer[user]
            
            for user, nvrs_list in to_send:
                nvrs_str = " e ".join(sorted(nvrs_list))
                ts = datetime.datetime.now().strftime('%H:%M:%S')
                agora_n = time.time()

                # Cooldown anti-spam: se já notificámos este login (user+nvrs) há menos de NOTIFY_COOLDOWN, ignorar
                cooldown_key = ('login', user, nvrs_str)
                ultima = last_notify_at.get(cooldown_key, 0)
                if agora_n - ultima < NOTIFY_COOLDOWN:
                    print(f"[{ts}] 🔕 Login {user}@{nvrs_str} suprimido (cooldown {int(NOTIFY_COOLDOWN - (agora_n - ultima))}s)")
                    continue
                last_notify_at[cooldown_key] = agora_n

                print(f"[{ts}] 📤 Notificação login: {user} em {nvrs_str}")
                last_activity_time = agora_n

                # Verificar se é hora anómala (login em horário pouco habitual)
                hora_atual = datetime.datetime.now().strftime('%H')
                anomalia_linha = ""
                if eh_hora_anomala(hora_atual):
                    if agora_n - anomalia_hora_last_alert >= ANOMALIA_HORA_COOLDOWN:
                        anomalia_hora_last_alert = agora_n
                        anomalia_linha = f"\n⚠️ *Hora atípica:* o Zé raramente acede às {hora_atual}h"
                        print(f"[{ts}] ⚠️ ANOMALIA: login em hora atípica ({hora_atual}h)")

                titulo = gerar_titulo_alerta()
                frase_local = gerar_frase_local(nvrs_list)
                frase_local_linha = f"\n_{frase_local}_" if frase_local else ""
                enviar_notificacao_telegram(
                    f"{titulo}\n\n"
                    f"🏢 *Locais:* {nvrs_str}\n"
                    f"👤 *Alvo:* `TARTARUGA GENIAL`\n"
                    f"⚡ *Estado:* 🟢 Sessão Ativa\n"
                    f"📅 *Hora:* `{ts}`"
                    f"{anomalia_linha}"
                    f"{frase_local_linha}"
                )
                
        except Exception as e:
            pass


def thread_logout_manager():
    """Processa logouts agrupados."""
    global last_activity_time
    
    while True:
        try:
            time.sleep(1)
            agora = time.time()
            to_send = []
            
            with lock:
                for user, info in list(logout_buffer.items()):
                    if agora - info['first_time'] > TEMPO_AGRUPAMENTO + 1:
                        to_send.append((user, list(info['nvrs'])))
                        del logout_buffer[user]
            
            for user, nvrs_list in to_send:
                nvrs_str = " e ".join(sorted(nvrs_list))
                ts = datetime.datetime.now().strftime('%H:%M:%S')
                agora_n = time.time()

                # Cooldown anti-spam: se já notificámos este logout (user+nvrs) há menos de NOTIFY_COOLDOWN, ignorar
                cooldown_key = ('logout', user, nvrs_str)
                ultima = last_notify_at.get(cooldown_key, 0)
                if agora_n - ultima < NOTIFY_COOLDOWN:
                    print(f"[{ts}] 🔕 Logout {user}@{nvrs_str} suprimido (cooldown {int(NOTIFY_COOLDOWN - (agora_n - ultima))}s)")
                    # Limpar atividade mesmo assim
                    with lock:
                        if user in user_activity:
                            del user_activity[user]
                    continue
                last_notify_at[cooldown_key] = agora_n

                print(f"[{ts}] 📤 Notificação logout: {user} de {nvrs_str}")
                last_activity_time = agora_n

                # Info de atividade
                activity_info = ""
                with lock:
                    if user in user_activity:
                        mode = user_activity[user].get("mode", "")
                        if mode == "playback":
                            activity_info = "\n📼 _Estava a ver gravações_"
                        # Limpar atividade
                        del user_activity[user]

                enviar_notificacao_telegram(
                    f"🛑 *LOGOUT DETETADO*\n\n"
                    f"🏢 *Locais:* {nvrs_str}\n"
                    f"👤 *Alvo:* `TARTARUGA GENIAL`\n"
                    f"ℹ️ *Motivo:* _{gerar_frase_saida()}_"
                    f"{activity_info}\n"
                    f"📅 *Hora:* `{ts}`"
                )
                
        except Exception as e:
            pass


def worker_pulse(gravador):
    """Worker principal de monitorização de um NVR."""
    nome_nvr = gravador["nome"]
    global last_activity_time
    fail_count = 0
    
    while True:
        try:
            usuarios = sondagem_ativa(gravador)
            ts = datetime.datetime.now().strftime('%H:%M:%S')

            # Falha de comunicação
            if usuarios is None:
                fail_count += 1
                if fail_count >= 5:
                    if nvr_status_cache.get(nome_nvr, True):
                        print(f"[{ts}] [NET FAIL] {nome_nvr} OFFLINE.")
                        enviar_notificacao_telegram(f"🚨 *ALERTA:* `{nome_nvr}` ficou OFFLINE!", force=True)
                        nvr_status_cache[nome_nvr] = False
                    time.sleep(30)
                    continue
                time.sleep(2)
                continue
            
            # Recuperação
            if not nvr_status_cache.get(nome_nvr, True):
                print(f"[{ts}] [NET OK] {nome_nvr} ONLINE.")
                enviar_notificacao_telegram(f"✅ *RECUPERADO:* `{nome_nvr}` voltou online.", force=True)
                nvr_status_cache[nome_nvr] = True
            
            fail_count = 0
            
            # Criar fingerprints
            fps_online = {}
            for u in usuarios:
                # Usar combinação única: nvr + ip + login_time
                chave = f"{nome_nvr}|{u['ip']}|{u['login_time']}"
                fps_online[chave] = u

            with lock:
                # DETEÇÃO DE SAÍDA
                chaves_a_remover = []
                for chave in sessoes_ativas:
                    if chave.startswith(f"{nome_nvr}|") and chave not in fps_online:
                        chaves_a_remover.append(chave)
                
                for chave in chaves_a_remover:
                    dados = sessoes_ativas[chave]
                    was_confirmed = dados.get('confirmed', False)
                    user_name = dados['user']
                    
                    if was_confirmed:
                        # Acumular tempo de sessão
                        duracao = time.time() - dados['first_seen']
                        daily_time[nome_nvr] = daily_time.get(nome_nvr, 0.0) + duracao
                        
                        # Verificar recorde de sessão
                        atualizar_recorde_sessao(duracao, nome_nvr)
                        
                        print(f"[{ts}] 🛑 SAÍDA: {user_name} @ {nome_nvr} (sessão: {formatar_tempo(duracao)})")
                        
                        if user_name not in logout_buffer:
                            logout_buffer[user_name] = {"nvrs": set(), "first_time": time.time()}
                        logout_buffer[user_name]["nvrs"].add(nome_nvr)
                    
                    del sessoes_ativas[chave]
                    
                    # Limpar de pendentes também
                    if chave in sessoes_pendentes:
                        del sessoes_pendentes[chave]

                # DETEÇÃO DE ENTRADA
                for chave, sess in fps_online.items():
                    if chave not in sessoes_ativas:
                        agora = time.time()
                        
                        sessoes_ativas[chave] = {
                            "user": sess['user'],
                            "ip": sess['ip'],
                            "first_seen": agora,
                            "nvr_nome": nome_nvr,
                            "confirmed": False,
                            "client_type": sess.get('client_type', 'unknown')
                        }
                        
                        sessoes_pendentes[chave] = {
                            "user": sess['user'],
                            "nvr_nome": nome_nvr,
                            "first_seen": agora
                        }
                        
                        print(f"[{ts}] 🎯 ENTRADA (pendente): {sess['user']} @ {nome_nvr} [{sess.get('client_type', '?')}]")

        except Exception as e:
            ts = datetime.datetime.now().strftime('%H:%M:%S')
            print(f"[{ts}] [WORKER ERROR] {nome_nvr}: {e}")
        
        time.sleep(1)


def thread_event_stream(gravador):
    """
    Monitoriza event stream do NVR para detetar atividade.
    Eventos de playback podem aparecer como:
    - RecordReplay, PlayBack, Playback
    - MediaFileFind, mediaFileFind
    - RecordBackup, FileBackup
    - Procura, Reproducao (PT-PT)
    """
    ip = gravador["ip"]
    nome_nvr = gravador["nome"]
    auth = HTTPDigestAuth(gravador["user"], gravador["pass"])
    url = f"http://{ip}/cgi-bin/eventManager.cgi?action=attach&codes=[All]&heartbeat=5"
    
    # Termos para detetar playback (inglês e português)
    PLAYBACK_TERMS = [
        "playback", "replay", "recordreplay", "mediafilefind", "filefind",
        "recordbackup", "filebackup", "download", "loadfile",
        "reproducao", "reprodução", "procura", "pesquisa", "gravacao", "gravação",
        "starttime", "endtime", "findfile", "findnext", "mediafile"
    ]
    
    # Termos para detetar liveview
    LIVEVIEW_TERMS = [
        "realmonitor", "liveview", "preview", "live", "stream",
        "direto", "directo", "tempo real", "visualizar"
    ]
    
    while True:
        try:
            with requests.Session() as s:
                with s.get(url, auth=auth, stream=True, timeout=120) as r:
                    if r.status_code == 200:
                        buffer = ""
                        for chunk in r.iter_content(chunk_size=4096):
                            if not chunk:
                                continue
                            
                            try:
                                text = chunk.decode('utf-8', errors='ignore')
                                buffer += text
                                
                                # Processar quando temos dados suficientes
                                while len(buffer) > 100:
                                    # Procurar fim de evento
                                    end_pos = -1
                                    for sep in ["\r\n\r\n", "\n\n", "--myboundary"]:
                                        pos = buffer.find(sep)
                                        if pos != -1 and (end_pos == -1 or pos < end_pos):
                                            end_pos = pos
                                    
                                    if end_pos == -1:
                                        # Manter só os últimos 500 chars
                                        if len(buffer) > 500:
                                            buffer = buffer[-500:]
                                        break
                                    
                                    event_data = buffer[:end_pos]
                                    buffer = buffer[end_pos+4:]
                                    event_lower = event_data.lower()
                                    
                                    # Verificar se há user alvo com sessão confirmada neste NVR
                                    with lock:
                                        users_ativos = []
                                        for chave, dados in sessoes_ativas.items():
                                            if chave.startswith(f"{nome_nvr}|") and dados.get('confirmed'):
                                                users_ativos.append(dados['user'])
                                    
                                    if not users_ativos:
                                        continue
                                    
                                    # Detetar PLAYBACK
                                    is_playback = any(term in event_lower for term in PLAYBACK_TERMS)
                                    
                                    if is_playback:
                                        ts = datetime.datetime.now().strftime('%H:%M:%S')
                                        
                                        for user in users_ativos:
                                            with lock:
                                                # Verificar cooldown
                                                agora = time.time()
                                                last_notify = 0
                                                if user in user_activity:
                                                    last_notify = user_activity[user].get('last_playback_notify', 0)
                                                
                                                if agora - last_notify > PLAYBACK_NOTIFY_COOLDOWN:
                                                    # Atualizar estado
                                                    if user not in user_activity:
                                                        user_activity[user] = {}
                                                    
                                                    old_mode = user_activity[user].get('mode', '')
                                                    user_activity[user]['mode'] = 'playback'
                                                    user_activity[user]['last_update'] = agora
                                                    user_activity[user]['nvr'] = nome_nvr
                                                    user_activity[user]['last_playback_notify'] = agora
                                                    
                                                    # Tentar extrair canal do event stream
                                                    canal_ev = extrair_canal_detalhe(event_data)
                                                    if canal_ev:
                                                        user_activity[user]['canal'] = canal_ev

                                                    canal_ev_str = f" [Canal {canal_ev}]" if canal_ev else ""
                                                    print(f"[{ts}] 📼 PLAYBACK detetado: {user} @ {nome_nvr}{canal_ev_str}")

                                                    # Enviar notificação
                                                    frase = gerar_frase_playback()
                                                    canal_info_ev = f"\n📹 *Canal:* {canal_ev}" if canal_ev else ""
                                                    enviar_notificacao_telegram(
                                                        f"📼 *PLAYBACK DETETADO*\n\n"
                                                        f"🏢 *Local:* {nome_nvr}\n"
                                                        f"👤 *Alvo:* `TARTARUGA GENIAL`{canal_info_ev}\n"
                                                        f"🔍 *Estado:* _{frase}_\n"
                                                        f"⏰ *Hora:* `{ts}`"
                                                    )
                                                    break
                                    
                                    # Detetar LIVEVIEW (com canal se possível)
                                    elif any(term in event_lower for term in LIVEVIEW_TERMS):
                                        for user in users_ativos:
                                            with lock:
                                                if user not in user_activity:
                                                    user_activity[user] = {}
                                                canal_lv_ev = extrair_canal_detalhe(event_data)
                                                if user_activity[user].get('mode') != 'liveview' or (canal_lv_ev and canal_lv_ev != user_activity[user].get('canal')):
                                                    user_activity[user]['mode'] = 'liveview'
                                                    if canal_lv_ev:
                                                        user_activity[user]['canal'] = canal_lv_ev
                                                    ts = datetime.datetime.now().strftime('%H:%M:%S')
                                                    canal_str_ev = f" [Canal {canal_lv_ev}]" if canal_lv_ev else ""
                                                    print(f"[{ts}] 📺 LiveView: {user} @ {nome_nvr}{canal_str_ev}")
                                                user_activity[user]['last_update'] = time.time()
                                                user_activity[user]['nvr'] = nome_nvr
                                            
                            except Exception:
                                pass
                    else:
                        time.sleep(10)
        except Exception as e:
            ts = datetime.datetime.now().strftime('%H:%M:%S')
            # Timeout é normal, não logar
            if "timeout" not in str(e).lower() and "read" not in str(e).lower():
                print(f"[{ts}] [STREAM] {nome_nvr}: {type(e).__name__}")
            time.sleep(5)


def thread_playback_detector(gravador):
    """
    Monitoriza logs do NVR via log.cgi para detetar playback.
    Procura entradas do tipo 'Reproduzir' (início playback) e 'Parar Reprodução' (fim).
    Confirmado via probe: os NVRs Dahua registam estas operações nos logs internos.
    """
    global tempo_playback_total

    ip = gravador["ip"]
    nome_nvr = gravador["nome"]
    auth = HTTPDigestAuth(gravador["user"], gravador["pass"])

    # Tipos de log que indicam playback (PT-PT conforme o probe confirmou)
    PLAYBACK_START_TYPES = ["reproduzir", "playback", "replay", "recordreplay"]
    PLAYBACK_STOP_TYPES = ["parar reprodução", "parar reproducao", "stop playback", "stop replay"]

    # Tipos de log para Live View
    LIVEVIEW_LOG_TYPES = ["visualizar", "preview", "realmonitor", "liveview", "ver em direto", "live"]

    # Tipos de log para PTZ
    PTZ_LOG_TYPES = ["ptz", "controlo ptz", "ptz control", "pan/tilt", "pantilt"]

    # Tipos de log para export/download/snapshot
    EXPORT_LOG_TYPES = ["exportar", "export", "download", "backup", "recordbackup", "filebackup", "descarregar"]
    SNAPSHOT_LOG_TYPES = ["snapshot", "captura", "capture", "fotografia", "foto", "print screen"]

    # Último timestamp processado (para não repetir notificações)
    last_processed_time = {}  # {user: last_log_timestamp_str}

    while True:
        try:
            time.sleep(15)  # Verificar a cada 15s

            # Tick acumulador: somar tempo decorrido para cada playback ainda em curso.
            # Não dependemos do log STOP do Dahua (raramente emitido); incrementamos a cada 15s.
            agora_tick = time.time()
            with lock:
                for u in list(playback_start_time.keys()):
                    delta = agora_tick - playback_start_time[u]
                    if delta > 0:
                        tempo_playback_total += delta
                        playback_start_time[u] = agora_tick
            
            # Só verificar se há user alvo ativo confirmado
            with lock:
                active_users = set()
                for chave, dados in sessoes_ativas.items():
                    if chave.startswith(f"{nome_nvr}|") and dados.get('confirmed'):
                        active_users.add(dados['user'])

            if not active_users:
                # Diagnóstico Palmeira: consultar logs mesmo sem active_users
                # para descobrir como aparece a atividade via DMSS cloud share
                if nome_nvr == "NVR Palmeira":
                    active_users = {"__palmeira_probe__"}
                else:
                    continue
            
            # Consultar logs dos últimos 2 minutos
            now = datetime.datetime.now()
            start = (now - datetime.timedelta(minutes=2)).strftime("%Y-%m-%d%%20%H:%M:%S")
            end = now.strftime("%Y-%m-%d%%20%H:%M:%S")
            
            # Iniciar pesquisa de logs
            start_url = f"http://{ip}/cgi-bin/log.cgi?action=startFind&condition.StartTime={start}&condition.EndTime={end}&condition.Type=All"
            
            try:
                resp = requests.get(start_url, auth=auth, timeout=8)
                if resp.status_code != 200 or "token" not in resp.text.lower():
                    continue
                
                # Extrair token
                token = None
                for line in resp.text.splitlines():
                    if "token" in line.lower() and "=" in line:
                        token = line.split("=", 1)[1].strip()
                        break
                
                if not token:
                    continue
                
                # Buscar últimos 50 logs
                find_url = f"http://{ip}/cgi-bin/log.cgi?action=doFind&token={token}&count=50"
                find_resp = requests.get(find_url, auth=auth, timeout=8)
                
                if find_resp.status_code == 200:
                    text = find_resp.text
                    
                    # Parsear entries de log
                    # Formato: items[N].Type=Reproduzir, items[N].User=ze, items[N].Time=...
                    entries = {}
                    for line in text.splitlines():
                        line = line.strip()
                        match = re.match(r'items\[(\d+)\]\.(\w+)=(.*)', line)
                        if match:
                            idx, field, value = match.groups()
                            if idx not in entries:
                                entries[idx] = {}
                            entries[idx][field] = value.strip()
                    
                    # Procurar entradas de playback dos users alvo
                    for idx in sorted(entries.keys(), key=int):
                        entry = entries[idx]
                        log_type = entry.get('Type', '').lower()
                        log_user = entry.get('User', '').lower()
                        log_time = entry.get('Time', '')
                        log_detail = entry.get('Detail', '')

                        # Logging defensivo Palmeira: capturar entries com user desconhecido
                        # (pode revelar 'admin', identificador de cloud share, email Dahua, etc.)
                        if nome_nvr == "NVR Palmeira" and log_user and log_user not in [u.lower() for u in active_users]:
                            entry_compacta = " | ".join(f"{k}={v}" for k, v in entry.items())
                            print(f"[PALMEIRA-LOG?] {entry_compacta[:600]}")

                        # Verificar se é playback START de um user alvo
                        if log_user in [u.lower() for u in active_users]:
                            is_playback_start = any(t in log_type for t in PLAYBACK_START_TYPES)
                            is_playback_stop = any(t in log_type for t in PLAYBACK_STOP_TYPES)
                            
                            if is_playback_start:
                                # Verificar se já processámos este evento
                                last_time = last_processed_time.get(log_user, '')
                                if log_time == last_time:
                                    continue  # Já processado
                                
                                last_processed_time[log_user] = log_time
                                
                                # Extrair canal do detalhe (suporta múltiplos formatos)
                                canal = extrair_canal_detalhe(log_detail) or '?'

                                # Extrair data e horário do ficheiro de playback
                                data_pb, h_start, h_end = extrair_horario_playback(log_detail)

                                ts = datetime.datetime.now().strftime('%H:%M:%S')

                                # Logging defensivo: se é playback mas detalhes não foram extraídos
                                if not (data_pb and h_start):
                                    print(f"[{ts}] ⚠️ Playback START sem horário: user={log_user} nvr={nome_nvr} type='{log_type}' detail='{log_detail[:300]}'")
                                
                                with lock:
                                    # Verificar cooldown
                                    agora = time.time()
                                    last_notify = 0
                                    if log_user in user_activity:
                                        last_notify = user_activity[log_user].get('last_playback_notify', 0)
                                    
                                    if agora - last_notify > PLAYBACK_NOTIFY_COOLDOWN:
                                        if log_user not in user_activity:
                                            user_activity[log_user] = {}
                                        
                                        user_activity[log_user]['mode'] = 'playback'
                                        user_activity[log_user]['last_update'] = agora
                                        user_activity[log_user]['nvr'] = nome_nvr
                                        user_activity[log_user]['last_playback_notify'] = agora
                                        
                                        # Registar início de playback para duração
                                        playback_start_time[log_user.lower()] = agora

                                        # Registar canal favorito
                                        if canal != '?':
                                            canais_playback[canal] = canais_playback.get(canal, 0) + 1

                                        # Guardar info de canal e período no user_activity
                                        user_activity[log_user]['canal'] = canal
                                        user_activity[log_user]['data_playback'] = data_pb
                                        user_activity[log_user]['periodo'] = f"{h_start}-{h_end}" if h_start and h_end else None

                                        # Gerar frase contextual baseada no horário
                                        frase_contexto = None
                                        horario_info = ""
                                        if h_start and h_end:
                                            frase_contexto = gerar_frase_playback_contextual(h_start, h_end, data_pb)
                                            # Incluir a DATA que está a ser vista
                                            if data_pb:
                                                try:
                                                    dt_pb = datetime.datetime.strptime(data_pb, '%Y-%m-%d')
                                                    dia_semana = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'][dt_pb.weekday()]
                                                    dias_atras = (datetime.datetime.now() - dt_pb).days
                                                    if dias_atras == 0:
                                                        data_label = "hoje"
                                                    elif dias_atras == 1:
                                                        data_label = "ontem"
                                                    else:
                                                        data_label = f"há {dias_atras} dias ({dia_semana} {data_pb[5:]})"
                                                    horario_info = f"\n📅 *A ver:* `{data_pb}` ({data_label})"
                                                    horario_info += f"\n🕐 *Período:* `{h_start} - {h_end}`"
                                                except:
                                                    horario_info = f"\n📅 *Data:* `{data_pb}`\n🕐 *Período:* `{h_start} - {h_end}`"
                                            else:
                                                horario_info = f"\n🕐 *Período:* `{h_start} - {h_end}`"

                                        print(f"[{ts}] 📼 PLAYBACK (log.cgi): {log_user} @ {nome_nvr} [Canal {canal}] [Data: {data_pb or '?'}] [{h_start or '?'}-{h_end or '?'}]")

                                        frase = frase_contexto or gerar_frase_playback()
                                        canal_info = f"📹 *Canal:* {canal}" if canal != '?' else ""
                                        enviar_notificacao_telegram(
                                            f"📼 *PLAYBACK DETETADO*\n\n"
                                            f"🏢 *Local:* {nome_nvr}\n"
                                            f"👤 *Alvo:* `TARTARUGA GENIAL`\n"
                                            f"{canal_info}{horario_info}\n"
                                            f"🔍 *Estado:* _{frase}_\n"
                                            f"⏰ *Hora atual:* `{ts}`"
                                        )
                                    else:
                                        print(f"[{ts}] 📼 Playback detetado (cooldown): {log_user} @ {nome_nvr}")
                            
                            elif is_playback_stop:
                                ts = datetime.datetime.now().strftime('%H:%M:%S')
                                # Calcular duração residual desde o último tick.
                                # O tick acumulador (no início do loop) já contabilizou a maior parte.
                                duracao_pb = 0
                                user_key = log_user.lower()
                                with lock:
                                    if user_key in playback_start_time:
                                        duracao_pb = time.time() - playback_start_time[user_key]
                                        if duracao_pb > 0:
                                            tempo_playback_total += duracao_pb
                                        del playback_start_time[user_key]
                                if duracao_pb > 0:
                                    print(f"[{ts}] ⏹️ Playback parado: {log_user} @ {nome_nvr} (resíduo: {formatar_tempo(duracao_pb)}, total: {formatar_tempo(tempo_playback_total)})")
                                else:
                                    print(f"[{ts}] ⏹️ Playback parado: {log_user} @ {nome_nvr}")
                                with lock:
                                    if log_user in user_activity and user_activity[log_user].get('mode') == 'playback':
                                        user_activity[log_user]['mode'] = 'liveview'
                                        user_activity[log_user]['canal'] = None
                                        user_activity[log_user]['data_playback'] = None
                                        user_activity[log_user]['periodo'] = None

                            else:
                                # === DETEÇÃO DE LIVE VIEW COM CANAL ===
                                is_liveview_log = any(t in log_type for t in LIVEVIEW_LOG_TYPES)
                                if is_liveview_log:
                                    canal_lv = extrair_canal_detalhe(log_detail)
                                    ts = datetime.datetime.now().strftime('%H:%M:%S')
                                    with lock:
                                        if log_user not in user_activity:
                                            user_activity[log_user] = {}
                                        old_canal = user_activity[log_user].get('canal')
                                        user_activity[log_user]['mode'] = 'liveview'
                                        user_activity[log_user]['last_update'] = time.time()
                                        user_activity[log_user]['nvr'] = nome_nvr
                                        if canal_lv:
                                            user_activity[log_user]['canal'] = canal_lv
                                            if canal_lv != old_canal:
                                                print(f"[{ts}] 📺 LiveView: {log_user} @ {nome_nvr} [Canal {canal_lv}]")
                                        else:
                                            # Não conseguiu extrair canal: limpa em vez de manter o anterior (que dava ilusão de "abre todos")
                                            user_activity[log_user]['canal'] = None
                                            print(f"[{ts}] 📺 LiveView SEM canal: {log_user} @ {nome_nvr} | type='{log_type}' | detail='{log_detail[:200]}'")

                                # === DETEÇÃO DE PTZ ===
                                is_ptz = any(t in log_type for t in PTZ_LOG_TYPES)
                                if is_ptz:
                                    canal_ptz = extrair_canal_detalhe(log_detail)
                                    ts = datetime.datetime.now().strftime('%H:%M:%S')
                                    with lock:
                                        if log_user not in user_operations:
                                            user_operations[log_user] = {'ptz_count': 0, 'export_count': 0, 'snapshot_count': 0, 'last_ptz_notify': 0, 'last_export_notify': 0}
                                        user_operations[log_user]['ptz_count'] += 1
                                        agora_ptz = time.time()
                                        # Notificar no máximo a cada 5 minutos
                                        if agora_ptz - user_operations[log_user].get('last_ptz_notify', 0) > 300:
                                            user_operations[log_user]['last_ptz_notify'] = agora_ptz
                                            canal_str = f" [Canal {canal_ptz}]" if canal_ptz else ""
                                            print(f"[{ts}] 🕹️ PTZ: {log_user} @ {nome_nvr}{canal_str}")
                                            enviar_notificacao_telegram(
                                                f"🕹️ *PTZ DETETADO*\n\n"
                                                f"🏢 *Local:* {nome_nvr}\n"
                                                f"👤 *Alvo:* `TARTARUGA GENIAL`\n"
                                                f"📹 *Canal:* {canal_ptz or '?'}\n"
                                                f"🔍 _Está a controlar uma câmara manualmente!_\n"
                                                f"⏰ *Hora:* `{ts}`"
                                            )

                                # === DETEÇÃO DE EXPORT/DOWNLOAD ===
                                is_export = any(t in log_type for t in EXPORT_LOG_TYPES)
                                if is_export:
                                    canal_exp = extrair_canal_detalhe(log_detail)
                                    data_exp, h_start_exp, h_end_exp = extrair_horario_playback(log_detail)
                                    ts = datetime.datetime.now().strftime('%H:%M:%S')
                                    with lock:
                                        if log_user not in user_operations:
                                            user_operations[log_user] = {'ptz_count': 0, 'export_count': 0, 'snapshot_count': 0, 'last_ptz_notify': 0, 'last_export_notify': 0}
                                        user_operations[log_user]['export_count'] += 1
                                        agora_exp = time.time()

                                        # Rolling window de exports recentes do user (anti-fuga)
                                        if log_user not in exports_recentes:
                                            exports_recentes[log_user] = []
                                        exports_recentes[log_user].append(agora_exp)
                                        # Remover exports fora da janela
                                        exports_recentes[log_user] = [t for t in exports_recentes[log_user] if agora_exp - t <= EXPORTS_JANELA]
                                        n_recent = len(exports_recentes[log_user])

                                        if agora_exp - user_operations[log_user].get('last_export_notify', 0) > 120:
                                            user_operations[log_user]['last_export_notify'] = agora_exp
                                            periodo_exp = ""
                                            if data_exp and h_start_exp and h_end_exp:
                                                periodo_exp = f"\n📅 *Ficheiro:* `{data_exp} {h_start_exp}-{h_end_exp}`"
                                            print(f"[{ts}] 💾 EXPORT: {log_user} @ {nome_nvr} [Canal {canal_exp or '?'}]{' ' + data_exp if data_exp else ''} (recentes: {n_recent})")
                                            enviar_notificacao_telegram(
                                                f"💾 *EXPORTAÇÃO DETETADA*\n\n"
                                                f"🏢 *Local:* {nome_nvr}\n"
                                                f"👤 *Alvo:* `TARTARUGA GENIAL`\n"
                                                f"📹 *Canal:* {canal_exp or '?'}{periodo_exp}\n"
                                                f"🔍 _Está a descarregar gravações!_\n"
                                                f"⏰ *Hora:* `{ts}`"
                                            )

                                        # ALERTA POSSÍVEL FUGA: muitos exports em janela curta
                                        if n_recent >= EXPORTS_RAJADA:
                                            last_rajada = exports_rajada_last_alert.get(log_user, 0)
                                            if agora_exp - last_rajada >= EXPORTS_RAJADA_COOLDOWN:
                                                exports_rajada_last_alert[log_user] = agora_exp
                                                janela_min = int(EXPORTS_JANELA // 60)
                                                print(f"[{ts}] 🚨 RAJADA DE EXPORTS: {log_user} fez {n_recent} downloads em {janela_min}min @ {nome_nvr}")
                                                enviar_notificacao_telegram(
                                                    f"🚨 *POSSÍVEL FUGA DE DADOS*\n\n"
                                                    f"O Zé fez *{n_recent} downloads* nos últimos {janela_min} minutos.\n"
                                                    f"Isto está fora do padrão habitual — pode estar a copiar gravações em massa.\n\n"
                                                    f"🏢 *Último local:* {nome_nvr}\n"
                                                    f"📊 *Total no dia:* {user_operations[log_user]['export_count']}\n"
                                                    f"⏰ *Hora:* `{ts}`",
                                                    force=True
                                                )

                                # === DETEÇÃO DE SNAPSHOT ===
                                is_snapshot = any(t in log_type for t in SNAPSHOT_LOG_TYPES)
                                if is_snapshot:
                                    canal_snap = extrair_canal_detalhe(log_detail)
                                    ts = datetime.datetime.now().strftime('%H:%M:%S')
                                    print(f"[{ts}] 📸 SNAPSHOT: {log_user} @ {nome_nvr} [Canal {canal_snap or '?'}]")
                
                # Limpar token
                try:
                    requests.get(f"http://{ip}/cgi-bin/log.cgi?action=stopFind&token={token}", 
                               auth=auth, timeout=5)
                except:
                    pass
                    
            except requests.exceptions.Timeout:
                pass
            except Exception as e:
                ts = datetime.datetime.now().strftime('%H:%M:%S')
                print(f"[{ts}] [PLAYBACK-LOG] {nome_nvr}: {e}")
                
        except Exception as e:
            ts = datetime.datetime.now().strftime('%H:%M:%S')
            if "timeout" not in str(e).lower():
                print(f"[{ts}] [!] Erro playback_detector {nome_nvr}: {e}")
            time.sleep(10)


def gerar_frase_sessao_longa(minutos):
    frases = [
        f"🐢 A tartaruga está colada ao DVR há {minutos} minutos!",
        f"🐢 {minutos} minutos a cuscar… isto já é um emprego.",
        f"🐢 O Zé está há {minutos} minutos sem desgrudar do ecrã.",
        f"🐢 Já vão {minutos} min de vigilância obsessiva do Zé.",
        f"🐢 Sessão maratona: {minutos} minutos e a contar!",
        f"🐢 O Zé deve ter pegado ao CCTV ({minutos} min).",
        f"🐢 Há {minutos} minutos que o Zé não larga as câmaras.",
        f"🐢 {minutos} minutos ininterruptos. Isto é dedicação ou obsessão?",
        f"🐢 O Zé vai bater recorde: {minutos} min sem parar!",
        f"🐢 Alguém que traga um café ao Zé, está há {minutos} min colado.",
        f"🐢 {minutos} min de sessão. O Zé deve estudar para o exame de CCTV.",
        f"🐢 Sessão longa detetada: {minutos} min. Será que tem almoços para fiscalizar?",
        f"🐢 {minutos} minutos ao vivo. A Netflix não tem nada disto.",
        f"🐢 O Zé está a bater recordes: {minutos} min no DVR.",
        f"🐢 Maratona de câmaras: {minutos} min e sem sinais de abrandamento.",
    ]
    return random.choice(frases)


def thread_long_session_checker():
    """Verifica sessões longas e envia alertas (10min e 2h)."""
    global long_session_alerted, very_long_session_alerted

    while True:
        try:
            time.sleep(30)  # Verificar a cada 30s

            if not is_within_schedule():
                continue

            agora = time.time()

            with lock:
                for chave, dados in sessoes_ativas.items():
                    if not dados.get('confirmed'):
                        continue

                    duracao = agora - dados['first_seen']

                    # Alerta de sessão MUITO longa (>2h) — vigia obsessiva
                    if duracao >= TEMPO_SESSAO_MUITO_LONGA:
                        last_vl = very_long_session_alerted.get(chave, 0)
                        if agora - last_vl >= TEMPO_SESSAO_MUITO_LONGA_COOLDOWN:
                            very_long_session_alerted[chave] = agora
                            horas = int(duracao // 3600)
                            mins = int((duracao % 3600) // 60)
                            ts_vl = datetime.datetime.now().strftime('%H:%M:%S')
                            print(f"[{ts_vl}] 🚨 SESSÃO MUITO LONGA (>2h): {dados['user']} @ {dados['nvr_nome']} ({horas}h{mins}min)")
                            enviar_notificacao_telegram(
                                f"🚨 *VIGIA OBSESSIVA DETETADA*\n\n"
                                f"O Zé está há *{horas}h{mins}min* sem largar o {dados['nvr_nome']}.\n"
                                f"Isto já parece preocupante — alguém precisa de o tirar de lá.\n\n"
                                f"🏢 *Local:* {dados['nvr_nome']}\n"
                                f"⏱️ *Duração:* {formatar_tempo(duracao)}\n"
                                f"⏰ *Hora:* `{ts_vl}`"
                            )

                    if duracao >= TEMPO_SESSAO_LONGA:
                        last_alert = long_session_alerted.get(chave, 0)
                        
                        if agora - last_alert >= TEMPO_SESSAO_LONGA_COOLDOWN:
                            long_session_alerted[chave] = agora
                            minutos = int(duracao // 60)
                            nvr_nome = dados['nvr_nome']
                            
                            ts = datetime.datetime.now().strftime('%H:%M:%S')
                            print(f"[{ts}] ⏰ SESSÃO LONGA: {dados['user']} @ {nvr_nome} ({minutos}min)")
                            
                            frase = gerar_frase_sessao_longa(minutos)
                            # Info de atividade atual
                            ua = user_activity.get(dados['user'], {})
                            mode = ua.get('mode', '?')
                            canal_sl = ua.get('canal')
                            activity_line = f"\n📺 *Modo:* {mode}"
                            if canal_sl:
                                activity_line += f" | 📹 Canal {canal_sl}"
                            data_pb_sl = ua.get('data_playback')
                            periodo_sl = ua.get('periodo')
                            if mode == 'playback' and (data_pb_sl or periodo_sl):
                                if data_pb_sl:
                                    activity_line += f"\n📅 *A ver:* {data_pb_sl}"
                                if periodo_sl:
                                    activity_line += f" ({periodo_sl})"
                            enviar_notificacao_telegram(
                                f"⏰ *SESSÃO LONGA*\n\n"
                                f"{frase}\n\n"
                                f"🏢 *Local:* {nvr_nome}\n"
                                f"👤 *Alvo:* `TARTARUGA GENIAL`\n"
                                f"⏱️ *Duração:* {formatar_tempo(duracao)}{activity_line}\n"
                                f"⏰ *Hora:* `{ts}`"
                            )
            
            # Limpar alertas de sessões que já terminaram
            with lock:
                chaves_ativas = set(sessoes_ativas.keys())
            long_session_alerted = {k: v for k, v in long_session_alerted.items() if k in chaves_ativas}
            very_long_session_alerted = {k: v for k, v in very_long_session_alerted.items() if k in chaves_ativas}
                    
        except Exception as e:
            ts = datetime.datetime.now().strftime('%H:%M:%S')
            print(f"[{ts}] [!] Erro long_session_checker: {e}")


def eh_hora_anomala(hora_str):
    """Decide se uma hora (e.g. '14') é anómala com base no histórico de acessos_por_hora.
    Critério: hora atual tem < ANOMALIA_HORA_THRESHOLD_PCT (5%) do total de acessos.
    Requer pelo menos 50 acessos no historial para ser fiável."""
    if not acessos_por_hora:
        return False
    total = sum(acessos_por_hora.values())
    if total < 50:
        return False  # Histórico insuficiente para decidir
    acessos_nesta_hora = acessos_por_hora.get(hora_str, 0)
    return (acessos_nesta_hora / total) < ANOMALIA_HORA_THRESHOLD_PCT


def thread_inactivity_checker():
    """Alerta se o Zé não aparece há >24h durante horário laboral."""
    global inactivity_last_alert

    while True:
        try:
            time.sleep(900)  # Verificar a cada 15 min

            if not is_within_schedule():
                continue

            agora = time.time()
            horas_sem_atividade = (agora - last_activity_time) / 3600

            if horas_sem_atividade >= INATIVIDADE_HORAS_ALERTA:
                if agora - inactivity_last_alert >= INATIVIDADE_COOLDOWN:
                    inactivity_last_alert = agora
                    ts = datetime.datetime.now().strftime('%H:%M:%S')
                    print(f"[{ts}] 😴 INATIVIDADE: Zé sem aparecer há {int(horas_sem_atividade)}h")
                    enviar_notificacao_telegram(
                        f"😴 *ZÉ EM MODO HIBERNAÇÃO*\n\n"
                        f"Já passaram *{int(horas_sem_atividade)}h* sem o Zé aparecer em nenhum DVR.\n"
                        f"Esqueceu-se do CCTV? Está de férias? Ou andamos a perder atividade?\n\n"
                        f"⏰ *Hora:* `{ts}`",
                        force=True
                    )
        except Exception as e:
            ts = datetime.datetime.now().strftime('%H:%M:%S')
            print(f"[{ts}] [!] Erro inactivity_checker: {e}")


def thread_data_saver():
    """Guarda dados periodicamente para persistência."""
    while True:
        try:
            time.sleep(SAVE_INTERVAL)
            guardar_dados()
        except Exception as e:
            ts = datetime.datetime.now().strftime('%H:%M:%S')
            print(f"[{ts}] [!] Erro data_saver: {e}")


# ==========================================
#  COMANDOS TELEGRAM
# ==========================================

def processar_comando_telegram(comando):
    """Processa um comando recebido via Telegram."""
    cmd = comando.strip().lower()
    
    if cmd == '/status':
        # Estado atual: só users alvo
        with lock:
            ativos = []
            for chave, dados in sessoes_ativas.items():
                if dados.get('confirmed') and dados['user'].lower() in [u.lower() for u in USERS_ALVO]:
                    duracao = time.time() - dados['first_seen']
                    ua = user_activity.get(dados['user'], {})
                    mode = ua.get('mode', 'desconhecido')
                    canal_atual = ua.get('canal')
                    canal_str = f" | 📹 Canal {canal_atual}" if canal_atual else ""
                    ativos.append(f"  👤 {dados['user']} @ {dados['nvr_nome']}")
                    ativos.append(f"     ⏱️ {formatar_tempo(duracao)} | 📺 {mode}{canal_str}")
                    # Mostrar se está em playback com detalhes
                    if dados['user'].lower() in playback_start_time:
                        pb_dur = time.time() - playback_start_time[dados['user'].lower()]
                        ativos.append(f"     📼 Playback há {formatar_tempo(pb_dur)}")
                    # Mostrar data e período do playback
                    data_pb = ua.get('data_playback')
                    periodo = ua.get('periodo')
                    if mode == 'playback' and (data_pb or periodo):
                        detalhes = []
                        if data_pb:
                            detalhes.append(f"data: {data_pb}")
                        if periodo:
                            detalhes.append(f"período: {periodo}")
                        ativos.append(f"     📅 A ver: {', '.join(detalhes)}")
        
        if ativos:
            msg = "🟢 *TARTARUGA ONLINE*\n\n" + "\n".join(ativos)
        else:
            msg = "🔴 *A tartaruga não está nos NVRs.*"
        
        # Tempo total de playback hoje
        if tempo_playback_total > 0:
            msg += f"\n\n📼 Playback hoje: {formatar_tempo(tempo_playback_total)}"
        
        # Estado dos NVRs
        msg += "\n\n📡 *NVRs:*"
        for nome, online in nvr_status_cache.items():
            icon = "✅" if online else "❌"
            msg += f"\n  {icon} {nome}"
        
        enviar_notificacao_telegram(msg, force=True)
        
    elif cmd == '/stats':
        # Estatísticas do dia
        protek = daily_stats.get('NVR Protek', 0)
        segunor = daily_stats.get('NVR Segunor', 0)
        palmeira = daily_stats.get('NVR Palmeira', 0)
        tp = daily_time.get('NVR Protek', 0)
        ts_time = daily_time.get('NVR Segunor', 0)
        tpal = daily_time.get('NVR Palmeira', 0)

        msg = f"📊 *ESTATÍSTICAS DE HOJE*\n\n"
        msg += f"🔢 Acessos: {protek + segunor + palmeira} ({protek} Protek / {segunor} Segunor / {palmeira} Palmeira)\n"
        msg += f"⏱️ Tempo: {formatar_tempo(tp + ts_time + tpal)} ({formatar_tempo(tp)} P / {formatar_tempo(ts_time)} S / {formatar_tempo(tpal)} Palm)\n"

        if missed_access_count_night > 0:
            msg += f"🌙 Acessos noturnos: {missed_access_count_night}\n"

        # Comparação com média
        media = calcular_media_semanal()
        if media and media['dias'] >= 3:
            media_total = sum(media['media_acessos'].values())
            if media_total > 0:
                ratio = (protek + segunor + palmeira) / media_total
                msg += f"\n📈 Vs média: {ratio:.0%} ({int(media_total)} acessos/dia em {media['dias']} dias)"

        enviar_notificacao_telegram(msg, force=True)
        
    elif cmd == '/recordes':
        msg = gerar_texto_recordes()
        enviar_notificacao_telegram(msg, force=True)
        
    elif cmd == '/canais':
        msg = gerar_texto_canais()
        enviar_notificacao_telegram(msg, force=True)
        
    elif cmd == '/semana':
        msg = gerar_relatorio_semanal()
        enviar_notificacao_telegram(msg, force=True)

    elif cmd == '/heatmap':
        heatmap = gerar_heatmap_texto()
        if heatmap:
            msg = f"🕐 *MAPA DE ATIVIDADE DE HOJE*\n{heatmap}"
        else:
            msg = "🕐 *MAPA DE ATIVIDADE*\n\n_Sem dados de hoje ainda._"
        enviar_notificacao_telegram(msg, force=True)

    elif cmd == '/hoje':
        # Resumo completo do dia atual
        protek = daily_stats.get('NVR Protek', 0)
        segunor = daily_stats.get('NVR Segunor', 0)
        palmeira = daily_stats.get('NVR Palmeira', 0)
        tp = daily_time.get('NVR Protek', 0)
        ts_time = daily_time.get('NVR Segunor', 0)
        tpal = daily_time.get('NVR Palmeira', 0)
        total = protek + segunor + palmeira
        tempo_total = tp + ts_time + tpal

        msg = f"📋 *RESUMO COMPLETO DE HOJE*\n\n"
        msg += f"🔢 *Acessos:* {total} ({protek} Protek / {segunor} Segunor / {palmeira} Palmeira)\n"
        msg += f"⏱️ *Tempo total:* {formatar_tempo(tempo_total)}\n"
        msg += f"   ↳ Protek: {formatar_tempo(tp)} | Segunor: {formatar_tempo(ts_time)} | Palmeira: {formatar_tempo(tpal)}\n"

        if tempo_playback_total > 0:
            msg += f"\n📼 *Playback:* {formatar_tempo(tempo_playback_total)}"

        # Sessões ativas agora
        with lock:
            ativos_agora = []
            for chave, dados in sessoes_ativas.items():
                if dados.get('confirmed') and dados['user'].lower() in [u.lower() for u in USERS_ALVO]:
                    duracao = time.time() - dados['first_seen']
                    mode = user_activity.get(dados['user'], {}).get('mode', '?')
                    canal_now = user_activity.get(dados['user'], {}).get('canal')
                    canal_str = f" [CH{canal_now}]" if canal_now else ""
                    ativos_agora.append(f"  🟢 {dados['nvr_nome']} ({formatar_tempo(duracao)}) {mode}{canal_str}")

        if ativos_agora:
            msg += f"\n\n🔴 *ONLINE AGORA:*\n" + "\n".join(ativos_agora)

        # Canais mais vistos hoje
        if canais_playback:
            top3 = sorted(canais_playback.items(), key=lambda x: x[1], reverse=True)[:3]
            canais_str = ", ".join(f"CH{k} ({v}x)" for k, v in top3)
            msg += f"\n\n📹 *Canais top:* {canais_str}"

        # Operações especiais
        ops_total = {}
        for user, ops in user_operations.items():
            if user.lower() in [u.lower() for u in USERS_ALVO]:
                for k, v in ops.items():
                    if k.endswith('_count') and v > 0:
                        ops_total[k] = ops_total.get(k, 0) + v
        if ops_total:
            ops_parts = []
            if ops_total.get('ptz_count', 0) > 0:
                ops_parts.append(f"🕹️ PTZ: {ops_total['ptz_count']}x")
            if ops_total.get('export_count', 0) > 0:
                ops_parts.append(f"💾 Exports: {ops_total['export_count']}x")
            if ops_total.get('snapshot_count', 0) > 0:
                ops_parts.append(f"📸 Snapshots: {ops_total['snapshot_count']}x")
            if ops_parts:
                msg += f"\n\n🔧 *Operações:* {' | '.join(ops_parts)}"

        # Heatmap
        heatmap = gerar_heatmap_texto()
        if heatmap:
            msg += f"\n{heatmap}"

        # Comparação com média
        comparacao = gerar_comparacao_media(protek, segunor, tp, ts_time, palmeira, tpal)
        if comparacao:
            msg += f"\n\n{comparacao}"

        if missed_access_count_night > 0:
            msg += f"\n\n🌙 Acessos noturnos: {missed_access_count_night}"

        enviar_notificacao_telegram(msg, force=True)

    elif cmd == '/comparar':
        msg = gerar_comparacao_semanal()
        enviar_notificacao_telegram(msg, force=True)

    elif cmd == '/help' or cmd == '/ajuda':
        msg = ("🤖 *COMANDOS DISPONÍVEIS*\n\n"
               "/status — Quem está online agora\n"
               "/stats — Estatísticas de hoje\n"
               "/hoje — Resumo completo do dia\n"
               "/semana — Relatório semanal\n"
               "/comparar — Esta semana vs anterior\n"
               "/heatmap — Mapa de atividade por hora\n"
               "/recordes — Recordes da tartaruga\n"
               "/canais — Canais favoritos de playback\n"
               "/help — Esta mensagem")
        enviar_notificacao_telegram(msg, force=True)
    else:
        return False
    
    return True


def thread_telegram_commands():
    """Thread que escuta comandos via Telegram (polling getUpdates)."""
    last_update_id = 0
    url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/getUpdates"
    
    while True:
        try:
            params = {
                "offset": last_update_id + 1,
                "timeout": 30,
                "allowed_updates": ["message"]
            }
            resp = requests.get(url, params=params, timeout=35)
            
            if resp.status_code == 200:
                data = resp.json()
                if data.get('ok') and data.get('result'):
                    for update in data['result']:
                        last_update_id = update['update_id']
                        
                        msg = update.get('message', {})
                        chat_id = str(msg.get('chat', {}).get('id', ''))
                        text = msg.get('text', '')
                        
                        # Só responder a mensagens do nosso chat
                        if chat_id == TELEGRAM_CHAT_ID and text.startswith('/'):
                            ts = datetime.datetime.now().strftime('%H:%M:%S')
                            print(f"[{ts}] 📱 Comando Telegram: {text}")
                            processar_comando_telegram(text)
            
            time.sleep(1)
            
        except requests.exceptions.Timeout:
            pass
        except Exception as e:
            ts = datetime.datetime.now().strftime('%H:%M:%S')
            if "timeout" not in str(e).lower():
                print(f"[{ts}] [!] Erro telegram_commands: {e}")
            time.sleep(10)


# ==========================================
#  AUTO-RESTART DE THREADS
# ==========================================

def criar_e_iniciar_thread(target, args=(), kwargs=None, name=""):
    """Cria, regista e inicia uma thread."""
    if kwargs is None:
        kwargs = {}
    t = threading.Thread(target=target, args=args, kwargs=kwargs, daemon=True, name=name)
    thread_registry.append({
        'thread': t,
        'target': target,
        'args': args,
        'kwargs': kwargs,
        'name': name,
        'restarts': 0
    })
    t.start()
    return t


def watchdog_restart_threads():
    """Verifica threads mortas e reinicia-as automaticamente."""
    restarted = []
    
    for entry in thread_registry:
        t = entry['thread']
        if not t.is_alive():
            ts = datetime.datetime.now().strftime('%H:%M:%S')
            entry['restarts'] += 1
            
            print(f"[{ts}] 🔄 AUTO-RESTART: {entry['name']} (restart #{entry['restarts']})")
            
            # Criar nova thread com os mesmos parâmetros
            new_t = threading.Thread(
                target=entry['target'],
                args=entry['args'],
                kwargs=entry['kwargs'],
                daemon=True,
                name=entry['name']
            )
            new_t.start()
            entry['thread'] = new_t
            restarted.append(entry['name'])
    
    if restarted:
        ts = datetime.datetime.now().strftime('%H:%M:%S')
        nomes = ", ".join(restarted)
        print(f"[{ts}] ✅ Threads reiniciadas: {nomes}")
        enviar_notificacao_telegram(
            f"🔄 *AUTO-RESTART*\n\nThreads reiniciadas: `{nomes}`",
            force=True
        )
    
    return len(restarted)


def main():
    print_banner()
    
    # Carregar dados persistentes
    carregar_dados()
    
    # Mensagem de arranque
    intro = gerar_frase_arranque()
    msg_startup = f"{intro}\n\n📊 *Estado dos Radares:*"
    
    for g in GRAVADORES:
        is_online = check_nvr_online(g)
        icon = "✅" if is_online else "❌"
        status = "ONLINE" if is_online else "OFFLINE"
        msg_startup += f"\n• {g['nome']}: {icon} {status}"
        nvr_status_cache[g['nome']] = is_online
    
    msg_startup += f"\n\n🛡️ *Anti-Handshake:* {TEMPO_MINIMO_SESSAO_REAL}s"
    msg_startup += f"\n📼 *Deteção Playback:* log.cgi (canal + data + período)"
    msg_startup += f"\n📺 *Deteção LiveView:* log.cgi (canal)"
    msg_startup += f"\n🕹️ *Deteção extra:* PTZ, Export, Snapshot"
    msg_startup += f"\n💾 *Persistência:* {len(history)} dias"
    msg_startup += f"\n📱 *Comandos:* /status /stats /hoje /semana /heatmap /recordes /canais"
    msg_startup += f"\n🔄 *Auto-restart:* Ativo"
    
    enviar_notificacao_telegram(msg_startup, force=True)
    
    # Baseline inicial
    print("[*] A sincronizar baseline...")
    for g in GRAVADORES:
        iniciais = sondagem_ativa(g)
        if iniciais:
            for u in iniciais:
                chave = f"{g['nome']}|{u['ip']}|{u['login_time']}"
                sessoes_ativas[chave] = {
                    "user": u['user'],
                    "ip": u['ip'],
                    "first_seen": time.time() - 120,
                    "nvr_nome": g['nome'],
                    "confirmed": True,
                    "client_type": u.get('client_type', 'unknown')
                }
                print(f"    ↳ BASELINE: {u['user']} @ {g['nome']}")

    print("[*] Monitorização ATIVA")
    print(f"    • Anti-handshake: {TEMPO_MINIMO_SESSAO_REAL}s")
    print(f"    • Agrupamento: {TEMPO_AGRUPAMENTO}s")
    print(f"    • Playback cooldown: {PLAYBACK_NOTIFY_COOLDOWN}s")
    print(f"    • Sessão longa: {TEMPO_SESSAO_LONGA}s")
    print(f"    • Persistência: cada {SAVE_INTERVAL}s")
    print(f"    • Comandos Telegram: /status /stats /hoje /semana /heatmap /recordes /canais")
    print(f"    • Deteção extra: LiveView canal, PTZ, Export, Snapshot")
    print(f"    • Auto-restart: Ativo")
    
    # Iniciar threads com auto-restart registry
    criar_e_iniciar_thread(thread_schedule_manager, name="ScheduleMgr")
    criar_e_iniciar_thread(thread_session_validator, name="SessionValidator")
    criar_e_iniciar_thread(thread_login_manager, name="LoginMgr")
    criar_e_iniciar_thread(thread_logout_manager, name="LogoutMgr")
    criar_e_iniciar_thread(thread_long_session_checker, name="LongSessionChk")
    criar_e_iniciar_thread(thread_inactivity_checker, name="InactivityChk")
    criar_e_iniciar_thread(thread_data_saver, name="DataSaver")
    criar_e_iniciar_thread(thread_telegram_commands, name="TelegramCmds")
    
    for g in GRAVADORES:
        criar_e_iniciar_thread(worker_pulse, args=(g,), name=f"Pulse-{g['nome']}")
        criar_e_iniciar_thread(thread_event_stream, args=(g,), name=f"Events-{g['nome']}")
        criar_e_iniciar_thread(thread_playback_detector, args=(g,), name=f"Playback-{g['nome']}")
    
    for entry in thread_registry:
        print(f"    ✓ Thread: {entry['name']}")
    
    print(f"\n[*] {len(thread_registry)} threads iniciadas. Ctrl+C para parar.\n")
    
    # Loop principal com watchdog + auto-restart
    while True:
        try:
            time.sleep(30)
            watchdog_restart_threads()
                    
        except KeyboardInterrupt:
            print("\n[*] A encerrar... Guardando dados.")
            guardar_dados()
            break


if __name__ == "__main__":
    main()