"""
FROST Server Client Module
Gestisce comunicazione con FROST Server e cache osservazioni
"""

import requests
import pandas as pd
from datetime import datetime, timedelta
from functools import lru_cache

TIMEOUT = 60


@lru_cache(maxsize=10000)
def parse_time_cached(value: str):
    """Parse time con cache per performance"""
    if value is None:
        return pd.NaT
    if isinstance(value, str) and "/" in value:
        return pd.to_datetime(value.split("/")[1], errors="coerce", utc=True)
    return pd.to_datetime(value, errors="coerce", utc=True)


def parse_time(value):
    """Wrapper per compatibilità"""
    if value is None or (isinstance(value, float) and pd.isna(value)):
        return pd.NaT
    if isinstance(value, str):
        return parse_time_cached(value)
    return pd.to_datetime(value, errors="coerce", utc=True)


class FROSTClient:
    """Client per FROST Server con gestione auth e caching"""
    
    def __init__(self, base_url, auth=None):
        self.base_url = base_url.rstrip('/')
        self.session = requests.Session()
        self.session.headers.update({'Accept-Encoding': 'gzip, deflate'})
        self.auth = auth 
        if auth:
            self.session.auth = auth

    def get(self, endpoint, params=None):
        """GET request con error handling"""
        try:
            url = f"{self.base_url}/{endpoint.lstrip('/')}"
            response = self.session.get(url, params=params, timeout=TIMEOUT)
            response.raise_for_status()
            data = response.json()
            return data if isinstance(data, dict) else {'value': data if isinstance(data, list) else []}
        except:
            return {}

    def get_thing(self, thing_id):
        """Carica un singolo Thing per ID"""
        tid = str(thing_id).strip()
        
        # Prova con quote
        data = self.get(f"Things('{tid}')")
        if data and '@iot.id' in data:
            return data
        
        # Fallback senza quote
        if tid.replace('.', '').replace('-', '').replace('_', '').isalnum():
            data = self.get(f"Things({tid})")
            if data and '@iot.id' in data:
                return data
        
        return None

    def get_datastreams_with_latest(self, thing_id):
        """Carica Datastreams CON l'ultima osservazione"""
        tid = str(thing_id).strip()
        
        params_expand = {
            '$expand': 'ObservedProperty,Observations($orderby=phenomenonTime desc;$top=1)',
            '$select': '@iot.id,name,unitOfMeasurement,phenomenonTime'
        }
        
        data = self.get(f"Things('{tid}')/Datastreams", params_expand)
        if not data.get('value'):
            if tid.replace('.', '').replace('-', '').replace('_', '').isalnum():
                data = self.get(f"Things({tid})/Datastreams", params_expand)
        
        if data.get('value'):
            has_observations = any(ds.get('Observations') for ds in data.get('value', []))
            if has_observations:
                return data.get('value', [])
        
        # Fallback senza observations
        params_basic = {
            '$expand': 'ObservedProperty',
            '$select': '@iot.id,name,unitOfMeasurement,phenomenonTime'
        }
        data = self.get(f"Things('{tid}')/Datastreams", params_basic)
        if not data.get('value'):
            if tid.replace('.', '').replace('-', '').replace('_', '').isalnum():
                data = self.get(f"Things({tid})/Datastreams", params_basic)
        
        return data.get('value', [])
    
    def get_datastreams(self, thing_id):
        """Carica i Datastreams per un Thing specifico"""
        tid = str(thing_id).strip()
        params = {
            '$expand': 'ObservedProperty',
            '$select': '@iot.id,name,unitOfMeasurement'
        }
        
        data = self.get(f"Things('{tid}')/Datastreams", params)
        if not data.get('value'):
            if tid.isdigit():
                data = self.get(f"Things({tid})/Datastreams", params)
        return data.get('value', [])

    def get_latest_observation(self, datastream_id):
        """Carica l'ultima osservazione per un Datastream"""
        ds = str(datastream_id).strip()
        params = {'$orderby': 'phenomenonTime desc', '$top': 1}
        
        data = self.get(f"Datastreams('{ds}')/Observations", params)
        if not data.get('value'):
            if ds.replace('.', '').replace('-', '').replace('_', '').isalnum():
                data = self.get(f"Datastreams({ds})/Observations", params)
        
        observations = data.get('value', [])
        return observations[0] if observations else None

    def get_observations_timerange(self, datastream_id, hours_back, limit=None, reference_time=None, select_clause='phenomenonTime,result'):
        """Carica osservazioni in una finestra temporale"""
        ds = str(datastream_id).strip()

        def _endpoint(ds_str: str) -> str:
            return f"Datastreams('{ds_str}')/Observations" if not ds_str.replace('.', '').replace('-', '').replace('_', '').isalnum() else f"Datastreams({ds_str})/Observations"

        def _page(endpoint: str, params: dict, hard_limit=None):
            observations = []
            data = self.get(endpoint, params)
            observations.extend(data.get('value', []))
            next_link = data.get('@iot.nextLink')

            while next_link and (hard_limit is None or len(observations) < hard_limit):
                try:
                    resp = self.session.get(next_link, timeout=TIMEOUT)
                    resp.raise_for_status()
                    data = resp.json()
                    batch = data.get('value', [])
                    if not batch:
                        break
                    observations.extend(batch)
                    next_link = data.get('@iot.nextLink')
                except Exception:
                    break

            return observations if hard_limit is None else observations[:hard_limit]

        endpoint = _endpoint(ds)

        if hours_back is None:
            params = {
                '$orderby': 'phenomenonTime asc',
                '$select': select_clause, 
                '$top': 2000
            }
            return _page(endpoint, params, hard_limit=limit)

        if reference_time is not None:
            end_time = reference_time
        else:
            latest_obs = self.get_latest_observation(datastream_id)
            if not latest_obs:
                return []
            latest_ts = parse_time(latest_obs.get('phenomenonTime'))
            if pd.isna(latest_ts):
                return []
            end_time = latest_ts.to_pydatetime().replace(tzinfo=None)

        start_time = (end_time - timedelta(hours=float(hours_back))).replace(microsecond=0)
        end_time = end_time.replace(microsecond=0)
        start_iso = start_time.strftime('%Y-%m-%dT%H:%M:%S.000Z')
        end_iso = end_time.strftime('%Y-%m-%dT%H:%M:%S.000Z')

        params = {
            '$orderby': 'phenomenonTime asc',
            '$filter': f'phenomenonTime ge {start_iso} and phenomenonTime le {end_iso}',
            '$select': select_clause, 
            '$top': 2000
        }
        return _page(endpoint, params, hard_limit=limit)
    
    
    def get_all_things(self, recent_hours=None, max_datastreams_check=3):
        """
        Carica Things e, se recent_hours è impostato, filtra quelli con almeno un Datastream
        aggiornato nelle ultime recent_hours ore.
        Controlla i primi N Datastreams (default=3) e si ferma appena ne trova uno valido.
        """
        # properties al $select
        params = {'$select': '@iot.id,name,description,properties', '$top': 1000}
        data = self.get('Things', params)
        things = data.get('value', [])

        if not recent_hours or not things:
            return things

        cutoff = datetime.utcnow() - timedelta(hours=float(recent_hours))
        filtered = []

        for thing in things:
            tid = str(thing.get('@iot.id')).strip()

            # phenomenonTime al $select
            ds_data = self.get(
                f"Things('{tid}')/Datastreams",
                {
                    '$select': '@iot.id,phenomenonTime',
                    '$top': max_datastreams_check
                }
            )
            datastreams = ds_data.get('value', [])
            
            # fallback per ID numerici
            if not datastreams and tid.replace('.', '').replace('-', '').replace('_', '').isalnum():
                ds_data = self.get(
                    f"Things({tid})/Datastreams",
                    {
                        '$select': '@iot.id,phenomenonTime',
                        '$top': max_datastreams_check
                    }
                )
                datastreams = ds_data.get('value', [])
                
            if not datastreams:
                continue

            # Controlla uno per uno, fermandoti appena ne trovi uno valido
            found_recent = False
            for ds in datastreams:
                phenom_time = ds.get('phenomenonTime')
                if phenom_time:
                    end_time = parse_time(phenom_time)
                    if not pd.isna(end_time) and end_time.to_pydatetime().replace(tzinfo=None) >= cutoff:
                        filtered.append(thing)
                        found_recent = True
                        break  # passa al prossimo Thing

        return filtered


class ObservationsCache:
    """Cache intelligente per osservazioni"""
    
    def __init__(self):
        self.cache = {}
    
    def clear(self):
        self.cache = {}
    
    def get_cache_key(self, hours_back, reference_time):
        """Genera chiave cache univoca per range temporale"""
        if hours_back is None:
            return 'all'
        if reference_time:
            ref_str = reference_time.strftime('%Y%m%d%H%M%S')
            return f'{hours_back}h_{ref_str}'
        hours_map = {24: '24h', 72: '72h', 168: '168h'}
        return hours_map.get(hours_back, f'{hours_back}h')
    
    def get_or_fetch(self, client, datastream_id, hours_needed=None, reference_time=None, select_clause='phenomenonTime,result'):
        """Recupera dati dalla cache o li scarica"""
        ds_id_str = str(datastream_id)
        self.cache.setdefault(ds_id_str, {})
        ds_cache = self.cache[ds_id_str]

        cache_key = self.get_cache_key(hours_needed, reference_time)

        # controllo se in cache
        if cache_key in ds_cache:
            cached_data = ds_cache[cache_key]
            
            # Verifica se le colonne richieste sono disponibili
            if cached_data and len(cached_data) > 0:
                cached_cols = set(cached_data[0].keys())
                requested_cols = set(select_clause.split(','))
                
                # Se tutte le colonne sono già presenti, usa la cache
                if requested_cols.issubset(cached_cols):
                    return cached_data

        # SCARICA SOLO SE NON IN CACHE O MANCANO COLONNE
        if hours_needed is None:
            ds_cache[cache_key] = client.get_observations_timerange(
                datastream_id, None, None, reference_time, select_clause
            )
            return ds_cache[cache_key]

        # Per range specifici
        ds_cache[cache_key] = client.get_observations_timerange(
            datastream_id, hours_needed, limit=None, reference_time=reference_time, select_clause=select_clause
        )
        return ds_cache[cache_key]

      



    # :)