import core.globals as g
# Auto-extracted from meshtastic_dashboard.py
import time
import threading
from typing import Dict, List, Optional, Tuple

_node_source_lock = threading.Lock()
_node_source_evidence: Dict[str, dict] = {}
_MQTT_CLOCK_SKEW_THRESHOLD = 45.0
_NULL_SNR_VALUES = {0, 0.0, None}
_NULL_RSSI_VALUES = {0, None}

def _update_node_source_evidence(node_id: str, source: str, snr=None, rssi=None):
    """Thread-safe update of per-node RF/MQTT evidence cache."""
    with _node_source_lock:
        if node_id not in _node_source_evidence:
            _node_source_evidence[node_id] = {
                "rf_confirmed": False,
                "mqtt_confirmed": False,
                "rf_snr_samples": [],
                "mqtt_count": 0,
                "rf_count": 0,
                "unknown_count": 0,
                "last_source": "UNKNOWN",
            }
        e = _node_source_evidence[node_id]
        e["last_source"] = source
        if source == "RF":
            e["rf_confirmed"] = True
            e["rf_count"] += 1
            if snr is not None and snr not in _NULL_SNR_VALUES:
                e["rf_snr_samples"].append(snr)
                if len(e["rf_snr_samples"]) > 20:
                    e["rf_snr_samples"] = e["rf_snr_samples"][-20:]
        elif source == "MQTT":
            e["mqtt_confirmed"] = True
            e["mqtt_count"] += 1
        else:
            e["unknown_count"] += 1


def _get_node_rf_history(node_id: str) -> Dict:
    with _node_source_lock:
        return _node_source_evidence.get(node_id, {}).copy()


class SourceEvidence:
    """
    Accumulates weighted evidence for/against RF vs MQTT.
    score > 0 = RF evidence
    score < 0 = MQTT evidence
    score == 0 = genuinely unknown
    """
    def __init__(self):
        self.rf_score = 0
        self.mqtt_score = 0
        self.reasons: List[str] = []
        self.hard_override: Optional[str] = None

    def add_rf(self, weight: int, reason: str):
        self.rf_score += weight
        self.reasons.append(f"RF+{weight}: {reason}")

    def add_mqtt(self, weight: int, reason: str):
        self.mqtt_score += weight
        self.reasons.append(f"MQTT+{weight}: {reason}")

    def set_definitive(self, source: str, reason: str):
        self.hard_override = source
        self.reasons.append(f"DEFINITIVE({source}): {reason}")

    def resolve(self) -> Tuple[str, float, List[str]]:
        if self.hard_override:
            return self.hard_override, 1.0, self.reasons
        total = self.rf_score + self.mqtt_score
        if total == 0:
            return "UNKNOWN", 0.0, self.reasons
        if self.rf_score > self.mqtt_score:
            confidence = self.rf_score / total
            if confidence >= 0.65:
                return "RF", confidence, self.reasons
            return "UNKNOWN", confidence, self.reasons
        if self.mqtt_score > self.rf_score:
            confidence = self.mqtt_score / total
            if confidence >= 0.65:
                return "MQTT", confidence, self.reasons
            return "UNKNOWN", confidence, self.reasons
        return "UNKNOWN", 0.5, self.reasons


def detect_packet_source(packet: dict, node_source_history: Dict = None) -> Tuple[str, float, List[str]]:
    ev = SourceEvidence()
    
    if packet.get("_synthetic_outbound"):
        ev.set_definitive("LOCAL", "synthetic outbound packet generated by dashboard")
        return ev.resolve()
    if packet.get("_from_sync"):
        ev.set_definitive("UNKNOWN", "node database sync  no per-packet transport data")
        return ev.resolve()
        
    via_mqtt_raw = (
        packet.get("viaMqtt")
        or packet.get("via_mqtt")
        or (isinstance(packet.get("decoded"), dict) and packet["decoded"].get("viaMqtt"))
        or (isinstance(packet.get("decoded"), dict) and packet["decoded"].get("via_mqtt"))
        or (isinstance(packet.get("raw"), dict) and packet["raw"].get("viaMqtt"))
    )
    
    if via_mqtt_raw is True:
        ev.add_mqtt(10, "viaMqtt=True set by firmware")
    elif via_mqtt_raw is False:
        ev.add_rf(8, "viaMqtt=False explicitly set by firmware")
        
    snr = packet.get("rxSnr")
    rssi = packet.get("rxRssi")
    has_real_snr = (snr is not None and snr not in _NULL_SNR_VALUES and isinstance(snr, (int, float)) and snr != 0.0)
    has_real_rssi = (rssi is not None and rssi not in _NULL_RSSI_VALUES and isinstance(rssi, (int, float)) and rssi != 0 and -150 < rssi < 0)
    
    if has_real_snr and has_real_rssi:
        ev.add_rf(9, f"real SNR={snr}dB and RSSI={rssi}dBm  packet physically received over air")
        if via_mqtt_raw is True:
            ev.add_rf(3, "MQTT-origin packet re-broadcast over RF and received by us directly")
    elif has_real_snr:
        ev.add_rf(6, f"real SNR={snr}dB  packet physically received over air")
    elif has_real_rssi:
        ev.add_rf(6, f"real RSSI={rssi}dBm  packet physically received over air")
    else:
        if via_mqtt_raw is not True:
            ev.add_mqtt(4, "no SNR/RSSI  packet not physically received by local radio")
            
    hop_limit = packet.get("hopLimit")
    hop_start = packet.get("hopStart")
    if hop_limit is not None and hop_start is not None:
        hops_consumed = hop_start - hop_limit
        if hops_consumed < 0:
            ev.add_mqtt(5, f"impossible hop delta: hopStart={hop_start} hopLimit={hop_limit}")
        elif hops_consumed == 0 and hop_start > 0 and not has_real_snr:
            ev.add_mqtt(3, f"zero hops consumed with no RF signal (hopStart={hop_start})")
        elif hops_consumed == 0 and has_real_snr:
            ev.add_rf(2, "direct RF neighbour  zero hops consumed with valid SNR")
        elif hops_consumed > 0:
            ev.add_rf(2, f"multi-hop RF path  {hops_consumed} hop(s) consumed")
        if hop_limit == 0 and not has_real_snr:
            ev.add_mqtt(2, "hop_limit=0 with no RF reception data")
            
    rx_time = packet.get("rxTime") or packet.get("timestamp")
    if rx_time:
        skew = abs(time.time() - float(rx_time))
        if skew > _MQTT_CLOCK_SKEW_THRESHOLD:
            ev.add_mqtt(2, f"clock skew {skew:.0f}s  possible MQTT relay delay or node clock drift")
        elif skew < 5.0 and has_real_snr:
            ev.add_rf(2, f"low clock skew {skew:.1f}s + real RF signal")
            
    want_ack = packet.get("wantAck")
    if want_ack is True and not has_real_snr:
        ev.add_mqtt(1, "wantAck=True but no RF reception data")
    elif want_ack is True and has_real_snr:
        ev.add_rf(1, "wantAck=True with valid RF reception  sender expects RF ACK")
        
    from_id = packet.get("fromId")
    if from_id and node_source_history:
        history = node_source_history
        if history.get("rf_confirmed") and not history.get("mqtt_confirmed"):
            ev.add_rf(3, f"node {from_id} historically RF-only ({history.get('rf_count', 0)} RF packets)")
        elif history.get("mqtt_confirmed") and not history.get("rf_confirmed"):
            ev.add_mqtt(3, f"node {from_id} historically MQTT-only ({history.get('mqtt_count', 0)} MQTT packets)")
        elif history.get("rf_snr_samples"):
            avg_snr = sum(history["rf_snr_samples"]) / len(history["rf_snr_samples"])
            if has_real_snr and abs(snr - avg_snr) < 15:
                ev.add_rf(2, f"SNR {snr}dB consistent with node's RF history (avg {avg_snr:.1f}dB)")
            elif has_real_snr and abs(snr - avg_snr) >= 15:
                ev.add_mqtt(1, f"SNR {snr}dB inconsistent with node RF history (avg {avg_snr:.1f}dB)")
                
    decoded = packet.get("decoded", {})
    portnum_str = str(decoded.get("portnum")) if isinstance(decoded, dict) and decoded.get("portnum") else ""
    if "ROUTING" in portnum_str and not has_real_snr:
        ev.add_mqtt(1, "ROUTING_APP packet with no RF reception  unusual")
    elif "ROUTING" in portnum_str and has_real_snr:
        ev.add_rf(2, "ACK/routing packet physically received  local mesh response")
    if "TRACEROUTE" in portnum_str:
        ev.add_rf(3, "TRACEROUTE_APP  always RF")
        
    return ev.resolve()


