"""
Module for fetching species occurrence data from multiple sources.

This module provides functions to retrieve species occurrence records from:
- Global Biodiversity Information Facility (GBIF)
- iNaturalist
- SpeciesLink

It supports filtering by geographic bounds, country, and year range, 
with caching to avoid redundant API calls.
"""

import os
import logging
import hashlib
from concurrent.futures import ThreadPoolExecutor
from typing import List, Tuple, Optional, Dict, Union

import pandas as pd
import requests
import ecoobs.inaturalist as inaturalist
import ecoobs.gbif as gbif
import ecoobs.speciesLink as speciesLink
from diskcache import Cache
from pathlib import Path

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

BASE_DIR = Path(__file__).resolve().parent
cache = Cache(BASE_DIR / ".all_cache")
def fetch_all(
    species_names: List[str],
    country: Optional[str] = None,
    year_range: Optional[Tuple[int, int]] = None,
    lat_min: Optional[float] = None,
    lat_max: Optional[float] = None,
    lon_min: Optional[float] = None,
    lon_max: Optional[float] = None,
    includeGbif: bool = True,
    includeInaturalist: bool = True,
    includeSpeciesLink: bool = True
) -> Tuple[pd.DataFrame, Dict, Dict]:
    """
    Fetch species occurrences from multiple data sources in parallel.
    
    Retrieves species occurrence data from GBIF, iNaturalist, and SpeciesLink
    simultaneously using ThreadPoolExecutor for improved performance.
    
    Args:
        species_names: List of species names to search for.
        country: Country code or name to filter occurrences (optional).
        year_range: Tuple of (start_year, end_year) to filter occurrences (optional).
        lat_min: Minimum latitude for geographic filtering (optional).
        lat_max: Maximum latitude for geographic filtering (optional).
        lon_min: Minimum longitude for geographic filtering (optional).
        lon_max: Maximum longitude for geographic filtering (optional).
        includeGbif: Whether to fetch data from GBIF (default: True).
        includeInaturalist: Whether to fetch data from iNaturalist (default: True).
        includeSpeciesLink: Whether to fetch data from SpeciesLink (default: True).
    
    Returns:
        Tuple of (gbif_data, inat_data, specieslink_data) as DataFrames/Dicts.
    
    Raises:
        Exception: If any source experiences an API error during fetch.
    """
    try:
        logger.info(f"Fetching species IDs/keys for {len(species_names)} species...")
        logger.info("Species IDs/keys retrieved successfully.")
    except Exception as e:
        logger.error(f"Error retrieving species IDs/keys: {e}")
        raise
    
    gbif_data = pd.DataFrame()
    inat_data = {}
    specieslink_data = {}
    
    try:
        with ThreadPoolExecutor(max_workers=3) as executor:
            futures = {}
            
            # Submit GBIF fetch if enabled
            if includeGbif:
                try:
                    gbif_keys = gbif.get_species_keys(species_names)
                    futures['gbif'] = executor.submit(
                        gbif.get_occurrences_by_key,
                        gbif_keys,
                        country,
                        year_range,
                        lat_min,
                        lat_max,
                        lon_min,
                        lon_max
                    )
                except Exception as e:
                    raise Exception(f"Error submitting GBIF fetch: {e}")
            
            # Submit iNaturalist fetch if enabled
            if includeInaturalist:
                try:
                    inat_ids = inaturalist.get_species_ids(species_names)
                    futures['inaturalist'] = executor.submit(
                        inaturalist.get_observations_by_id,
                        inat_ids,
                        country,
                        year_range,
                        lat_min,
                        lat_max,
                        lon_min,
                        lon_max
                    )
                except Exception as e:
                    raise Exception(f"Error submitting iNaturalist fetch: {e}")
            
            # Submit SpeciesLink fetch if enabled
            if includeSpeciesLink:
                try:
                    futures['specieslink'] = executor.submit(
                        speciesLink.get_occurrences_by_name,
                        species_names,
                        country,
                        year_range,
                        lat_min,
                        lat_max,
                        lon_min,
                        lon_max
                    )
                except Exception as e:
                    raise Exception(f"Error submitting SpeciesLink fetch: {e}")
            
            # Collect results as they complete
            for source, future in futures.items():
                try:
                    result = future.result()
                    if source == 'gbif':
                        gbif_data = result if isinstance(result, pd.DataFrame) else pd.DataFrame()
                        logger.info(f"GBIF: Retrieved {len(gbif_data)} occurrences.")
                    elif source == 'inaturalist':
                        inat_data = result if isinstance(result, (dict, list)) else {}
                        logger.info(f"iNaturalist: Retrieved {len(inat_data)} occurrences.")
                    elif source == 'specieslink':
                        specieslink_data = result if isinstance(result, (dict, list)) else {}
                        logger.info(f"SpeciesLink: Retrieved  {len(specieslink_data)} occurrences.")
                except Exception as e:
                    raise Exception(f"Error retrieving {source} data: {e}")
        
        logger.info("All data sources fetched successfully.")
    except Exception as e:
        logger.error(f"Critical error in fetch_all: {e}")
        raise
    
    return gbif_data, inat_data, specieslink_data

def countryCode(country: str, force_refresh: bool = False, cache_expire: int = None):
    """
    Get country code (ISO2) using REST Countries API with persistent cache.

    Args:
        country (str): Country name or ISO2.
        force_refresh (bool): Ignore cache and force API call.
        cache_expire (int): Expiration time in seconds (optional).
    """

    if pd.isna(country):
        return None

    country = str(country).strip()

    if len(country) == 2:
        return country.upper()

    cache_key = f"country_code_{country.lower()}"

    if cache_key in cache and not force_refresh:
        return cache[cache_key]

    url = f"https://restcountries.com/v3.1/name/{country.title()}"

    try:
        response = requests.get(url, timeout=5)

        if response.status_code != 200:
            return f"{country} (code not found)"

        result = response.json()
        code = result[0].get("cca2")

        if cache_expire:
            cache.set(cache_key, code, expire=cache_expire)
        else:
            cache[cache_key] = code

        return code

    except Exception:
        return None
        
def date_parser(year, month, day):
    try:
        return f"{int(year)}-{int(month):02d}-{int(day):02d}"
    except:
        return None
    
     
def get_occurrences(
    species_names: List[str],
    country: Optional[str] = None,
    year_range: Optional[Tuple[int, int]] = None,
    lat_min: Optional[float] = None,
    lat_max: Optional[float] = None,
    lon_min: Optional[float] = None,
    lon_max: Optional[float] = None,
    includeGbif: bool = True,
    includeInaturalist: bool = True,
    includeSpeciesLink: bool = True
) -> pd.DataFrame:
    """
    Retrieve species occurrence records from multiple sources with caching.
    
    Fetches occurrence data for specified species from available sources, with
    support for geographic and temporal filtering. Results are cached to avoid
    redundant API calls. If cached data exists, it is returned immediately.
    
    Args:
        species_names: List of species names to search for (required, must not be empty).
        country: Country filter for occurrences (optional).
        year_range: Tuple of (start_year, end_year) for temporal filtering (optional).
        lat_min: Minimum latitude boundary (optional).
        lat_max: Maximum latitude boundary (optional).
        lon_min: Minimum longitude boundary (optional).
        lon_max: Maximum longitude boundary (optional).
        includeGbif: Include GBIF data source (default: True).
        includeInaturalist: Include iNaturalist data source (default: True).
        includeSpeciesLink: Include SpeciesLink data source (default: True).
    
    Returns:
        pd.DataFrame: Combined occurrence data from all enabled sources, or empty
                      DataFrame if an error occurs.
    
    Raises:
        ValueError: If species_names is empty or no data sources are enabled.
        IOError: If cache directory cannot be created.
    
    Examples:
        >>> df = get_occurrences(['Puma concolor'], country='Brazil')
        >>> df = get_occurrences(['Apis mellifera'], year_range=(2020, 2023))
        >>> df = get_occurrences(['Panthera onca'], lat_min=-5, lat_max=5, 
        ...                      lon_min=-60, lon_max=-55)
    """
    
    # Validate input parameters
    if not species_names or (isinstance(species_names, list) and len(species_names) == 0):
        logger.error("species_names must be a non-empty list of species names.")
        raise ValueError("species_names must be a non-empty list of species names.")
    
    if not (includeGbif or includeInaturalist or includeSpeciesLink):
        logger.error("At least one data source must be included.")
        raise ValueError("At least one data source must be included.")
    
    logger.info(f"Starting occurrence data retrieval for {len(species_names)} species.")
    
    
    source_parts = []
    if includeGbif:
        source_parts.append('gbif')
    if includeInaturalist:
        source_parts.append('inaturalist')
    if includeSpeciesLink:
        source_parts.append('specieslink')
                
    # Fetch fresh data from sources
    try:
        logger.info("Fetching fresh data from sources...")
        gbif_data, inat_data, specieslink_data = fetch_all(
            species_names,
            country,
            year_range,
            lat_min,
            lat_max,
            lon_min,
            lon_max,
            includeGbif,
            includeInaturalist,
            includeSpeciesLink
        )
        
        # Convert all data to DataFrames
        try:
            df_inat = pd.DataFrame(inat_data) if inat_data else pd.DataFrame()
        except Exception as e:
            logger.warning(f"Error converting iNaturalist data: {e}")
            df_inat = pd.DataFrame()
        
        try:
            df_specieslink = pd.DataFrame(specieslink_data) if specieslink_data else pd.DataFrame()
        except Exception as e:
            logger.warning(f"Error converting SpeciesLink data: {e}")
            df_specieslink = pd.DataFrame()
        
        # Combine all DataFrames
        try:
            dfs_to_combine = [gbif_data, df_inat, df_specieslink]
            dfs_to_combine = [df for df in dfs_to_combine if not df.empty]
            
            if not dfs_to_combine:
                logger.warning("No data retrieved from any source.")
                return pd.DataFrame()
            
            df = pd.concat(dfs_to_combine, ignore_index=True)
            logger.info(f"Combined {len(df)} total records from all sources.")
        except Exception as e:
            logger.error(f"Error combining DataFrames: {e}")
            return pd.DataFrame()
        
        df = adjust_data(df)
        return df
        
    except Exception as e:
        logger.error(f"Critical error retrieving occurrences: {e}", exc_info=True)
        return pd.DataFrame()
    
    
def adjust_data(df):
    
    df = df.dropna()
    
    
    codes = (
        df['country']
        .dropna()
        .unique()
    )
    mapping = {c: countryCode(c) for c in codes}
    df = df.copy()
    df['country'] = df['country'].map(mapping)
    df['year'] = df['year'].map(lambda x: int(x))
    df['eventDate'] = df[['year', 'month', 'day']].apply(lambda row: date_parser(*row), axis=1)
    df = df.sort_values('eventDate', ascending=True)
    
    return df


def clear_cache(species_link: bool = True, inaturalist: bool = True, gbif: bool = True):
    if species_link:
        Cache(BASE_DIR / ".specieslink_cache").clear()
    if inaturalist:
        Cache(BASE_DIR / ".inaturalist_cache").clear()
    if gbif:
        Cache(BASE_DIR / ".gbif_cache").clear()
        
    if gbif and species_link and inaturalist:
        Cache(BASE_DIR / ".all_cache").clear()