"""
FROST Utils Module
Funzioni di utilità condivise per le applicazioni FROST
"""

import requests
import pandas as pd
import unicodedata
import html
from math import isfinite
from datetime import datetime, timedelta
import tzlocal

TIMEOUT = 30



# TEXT NORMALIZATION


def norm_text(s: str) -> str:
    """Normalizza testo per confronti case-insensitive"""
    if not s:
        return ""
    s = html.unescape(str(s))
    s = unicodedata.normalize("NFKD", s)
    s = "".join([c for c in s if not unicodedata.combining(c)])
    return s.lower().strip()



# VALUE FORMATTING


def format_value(val):
    """Formatta valori numerici con separatori italiani"""
    try:
        v = float(val)
        if not isfinite(v):
            return "—"
        return f"{v:,.0f}".replace(",", ".") if abs(v) >= 100 else f"{v:,.2f}".replace(",", ".")
    except:
        return str(val)


def format_timestamp(dt, timezone='Europe/Rome'):
    """Formatta timestamp in base al timezone selezionato"""
    if dt is None or pd.isna(dt):
        return "N/A"
    
    # Converti a pandas Timestamp se necessario
    if isinstance(dt, datetime):
        if dt.tzinfo is None:
            dt = pd.Timestamp(dt, tz='UTC')
        else:
            dt = pd.Timestamp(dt).tz_convert('UTC')
    elif isinstance(dt, pd.Timestamp):
        if dt.tz is None:
            dt = dt.tz_localize('UTC')
        elif dt.tz != 'UTC':
            dt = dt.tz_convert('UTC')
    else:
        dt = pd.to_datetime(dt, utc=True)
    
    # Converti al timezone richiesto
    if timezone == 'local':
        try:
            local_tz = tzlocal.get_localzone()
            dt = dt.tz_convert(str(local_tz))
        except:
            dt = dt.tz_convert('Europe/Rome')
    elif timezone == 'UTC':
        dt = dt.tz_convert('UTC')
    else:
        dt = dt.tz_convert(timezone)
    
    return dt.strftime("%d %B %Y, %H:%M")



# STANDARD SET FUNCTIONS


def load_standard(url: str):
    """Carica standard set JSON da URL"""
    try:
        response = requests.get(url, timeout=TIMEOUT)
        response.raise_for_status()
        return response.json()
    except:
        return None


def build_alias_index(standard: dict):
    """Costruisce indice di alias per riconoscimento datastream"""
    index = {}
    for ds in standard.get("datastreamLabels", []):
        ds_name = ds.get("datastreamName", "")
        for alias in ([ds_name] + ds.get("datastreamNameAliases", [])):
            alias_norm = norm_text(alias)
            if alias_norm:
                index[alias_norm] = ds_name
    return index


def detect_measure(name: str, alias_index: dict, datastream_labels: list):
    """Rileva datastream corrispondente basandosi su nome/alias"""
    low_name = norm_text(name or "")
    
    # Cerca corrispondenza esatta con alias
    for alias, ds_name in alias_index.items():
        if alias and alias == low_name:
            return ds_name
    
    # Cerca substring match (dal più lungo al più corto)
    for alias, ds_name in sorted(alias_index.items(), key=lambda x: len(x[0]), reverse=True):
        if alias and len(alias) > 2 and alias in low_name:
            return ds_name
    
    return None


def get_datastream_config(ds_name: str, datastream_labels: list):
    """Ottiene configurazione completa datastream dallo standard"""
    for ds in datastream_labels:
        if ds.get("datastreamName") == ds_name:
            return ds
    return None


def classify_value(val: float, thresholds: list, greater_worse: bool, n_cat: int):
    """Classifica valore in base a threshold e restituisce categoria e progress"""
    if val is None:
        return None, 0.0
    try:
        v = float(val)
        if not isfinite(v):
            return None, 0.0
        
        th = thresholds[:] if greater_worse else thresholds[::-1]
        n = max(n_cat, len(th) + 1) if th else max(n_cat, 1)
        
        if th:
            for idx, b in enumerate(th):
                if v < b:
                    prev = th[idx-1] if idx > 0 else None
                    span = (b - prev) if prev is not None else (b or 1.0)
                    intra = (v - (prev or 0.0)) / (span or 1.0)
                    intra = max(0.0, min(1.0, intra))
                    cat = idx if greater_worse else (n - 1 - idx)
                    progress = (idx + intra) / n
                    return cat, max(0.0, min(1.0, progress))
            return (n-1) if greater_worse else 0, 1.0
        return None, 0.0
    except:
        return None, 0.0



# OBSERVATIONS AGGREGATION


def aggregate_observations_optimized(obs_list, aggregation='none', tz='Europe/Rome'):
    """
    Aggrega osservazioni con logica corretta nel fuso 'tz':
    - Oraria: 13:00:00 (incluso) → 13:59:59 (escluso) → media a 13:00
    - Giornaliera: 00:00:00 (incluso) → 23:59:59 (escluso) → media a 00:00
    """
    from frost_client import parse_time
    
    if aggregation == 'none' or not obs_list:
        return obs_list
    
    data = []
    for o in obs_list:
        t = parse_time(o.get('phenomenonTime'))
        v = o.get('result')
        if not pd.isna(t) and v is not None:
            try:
                data.append({'time': t, 'value': float(v)})
            except:
                continue
    
    if not data:
        return obs_list
    
    df = pd.DataFrame(data)
    df = df.dropna(subset=['value'])
    
    if df.empty:
        return obs_list
    
    # Converti al timezone specificato
    df['time'] = df['time'].dt.tz_convert(tz)
    
    # Aggrega con floor
    if aggregation == 'hourly':
        df['group'] = df['time'].dt.floor('H')
    elif aggregation == 'daily':
        df['group'] = df['time'].dt.floor('D')
    else:
        return obs_list
    
    aggregated = df.groupby('group', as_index=False).agg({'value': 'mean'})
    aggregated.columns = ['time', 'value']
    
    return [{'phenomenonTime': row['time'].isoformat(), 'result': row['value']} 
            for _, row in aggregated.iterrows()]



# TIME RANGE PARSING


def parse_phenomenon_time_range(phenom_time_str):
    """Estrae start e end da phenomenonTime formato 'start/end'"""
    from frost_client import parse_time
    
    if not phenom_time_str or '/' not in phenom_time_str:
        return None, None
    try:
        parts = phenom_time_str.split('/')
        start = parse_time(parts[0])
        end = parse_time(parts[1])
        return (start if not pd.isna(start) else None, 
                end if not pd.isna(end) else None)
    except:
        return None, None



# PROPERTY EXTRACTION (per monitor app)


def extract_property_keys(things):
    """Estrae chiavi uniche da properties di tutti i things"""
    keys = set()
    for thing in things:
        props = thing.get('properties', {})
        if props and isinstance(props, dict):
            keys.update(props.keys())
    return sorted(list(keys))


def extract_property_values(things, key):
    """Estrae valori unici per una specifica chiave di properties"""
    values = set()
    for thing in things:
        props = thing.get('properties', {})
        if props and isinstance(props, dict) and key in props:
            val = props[key]
            if val is not None:
                values.add(str(val))
    return sorted(list(values))


def filter_things_by_property(things, key, value):
    """Filtra things in base a property key e value"""
    if not key or not value:
        return things
    
    filtered = []
    for thing in things:
        props = thing.get('properties', {})
        if props and isinstance(props, dict) and key in props:
            if str(props[key]) == value:
                filtered.append(thing)
    return filtered




# THING INFORMATION EXTRACTION (Location e FeaturesOfInterest)


def get_thing_locations(client, thing_id):
    """Carica locations associate a un Thing"""
    try:
        tid = str(thing_id).strip()
        
        # Tenta con quoting
        data = client.get(f"Things('{tid}')/Locations", {'$expand': 'HistoricalLocations', '$top': 100})
        if not data.get('value'):
            # Fallback numerico
            if tid.replace('.', '').replace('-', '').replace('_', '').isalnum():
                data = client.get(f"Things({tid})/Locations", {'$expand': 'HistoricalLocations', '$top': 100})
        
        return data.get('value', [])
    except:
        return []


def get_thing_features_of_interest(client, thing_id):
    """Carica FeaturesOfInterest associate a un Thing tramite le sue Observations"""
    try:
        tid = str(thing_id).strip()
        
        # Carica Datastreams per il Thing
        ds_data = client.get(f"Things('{tid}')/Datastreams", {'$select': '@iot.id', '$top': 1000})
        if not ds_data.get('value'):
            if tid.replace('.', '').replace('-', '').replace('_', '').isalnum():
                ds_data = client.get(f"Things({tid})/Datastreams", {'$select': '@iot.id', '$top': 1000})
        
        datastreams = ds_data.get('value', [])
        foi_set = {}  # Usa dict per evitare duplicati, chiave = @iot.id
        
        # Per ogni datastream, carica le Observations CON expand di FeatureOfInterest
        for ds in datastreams:
            ds_id = str(ds.get('@iot.id'))
            
            # Usa $expand per caricare direttamente il FeatureOfInterest
            obs_params = {
                '$expand': 'FeatureOfInterest',
                '$select': '@iot.id',  # Observation serve solo per il link
                '$top': 10  # Poche observations bastano per trovare i FOI
            }
            
            obs_data = client.get(f"Datastreams('{ds_id}')/Observations", obs_params)
            if not obs_data.get('value'):
                obs_data = client.get(f"Datastreams({ds_id})/Observations", obs_params)
            
            # Estrai i FeatureOfInterest unici
            for obs in obs_data.get('value', []):
                foi = obs.get('FeatureOfInterest')
                if foi and '@iot.id' in foi:
                    foi_id = foi['@iot.id']
                    if foi_id not in foi_set:
                        foi_set[foi_id] = foi
        
        return list(foi_set.values())
    except Exception as e:
        import streamlit as st
        st.error(f"Error loading features: {str(e)}")
        return []

def flatten_nested_dict(data, prefix=""):
    """Appiattisce dizionari annidati per visualizzazione, ma mantiene la gerarchia"""
    result = []
    
    if isinstance(data, dict):
        for key, value in data.items():
            if key.startswith('@iot') or key.endswith('navigationLink'):
                continue
            
            if isinstance(value, dict):
                result.append({
                    'key': key,
                    'value': None,
                    'children': flatten_nested_dict(value, f"{prefix}{key}/"),
                    'is_parent': True
                })
            elif isinstance(value, list):
                if value and isinstance(value[0], dict):
                    result.append({
                        'key': key,
                        'value': f"[{len(value)} items]",
                        'children': [],
                        'is_parent': False
                    })
                else:
                    result.append({
                        'key': key,
                        'value': str(value),
                        'children': [],
                        'is_parent': False
                    })
            else:
                result.append({
                    'key': key,
                    'value': str(value) if value is not None else "—",
                    'children': [],
                    'is_parent': False
                })
    
    return result






# :)