#!/usr/bin/python3

"""
Copyright (c) 2023 NIBIO <http://www.nibio.no/>. 

This file is part of VIPSCore-Python-Common.
VIPSCore-Python-Common is free software: you can redistribute it and/or modify
it under the terms of the NIBIO Open Source License as published by 
NIBIO, either version 1 of the License, or (at your option) any
later version.
 
VIPSCore-Python-Common is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
NIBIO Open Source License for more details.

You should have received a copy of the NIBIO Open Source License
along with VIPSCore-Python-Common.  If not, see <http://www.nibio.no/licenses/>.
"""

# 
# @author Tor-Einar Skog <tor-einar.skog@nibio.no>
# https://www.pedaldrivenprogramming.com/2020/12/fastapi-fundamentals-pydantic/

from datetime import datetime
from shapely.geometry import Point, Polygon
from pydantic import BaseModel, validator, constr, Field
from typing import Any, ClassVar, Optional
import json
import pytz


class Result(BaseModel):
    """
    Represents a set of DSS model result values for a given point in space (Point, Polygon, MultiPolygon) and time (Period or immediate) 
    """
    valid_time_start: datetime = Field(alias="validTimeStart")
    valid_time_end: Optional[datetime] = Field(alias="validTimeEnd")
    valid_geometry: Optional[Any] = Field(alias="validGeometry")
    warning_status: int = Field(alias="warningStatus")
    all_values: str = Field("{}", alias="allValues")

    class Config:
        allow_population_by_field_name = True


    WARNING_STATUS_NO_WARNING:              ClassVar[int] = 0
    WARNING_STATUS_NO_WARNING_MISSING_DATA: ClassVar[int] = 1
    WARNING_STATUS_NO_RISK:                 ClassVar[int] = 2
    WARNING_STATUS_MINOR_RISK:              ClassVar[int] = 3
    WARNING_STATUS_HIGH_RISK:               ClassVar[int] = 4
    

    def get_keys(self):
        return set(json.loads(self.all_values.keys)) if self.all_values is not None else set()

    @validator("valid_time_start")
    def ensure_timezone(cls, v):
        if v.tzinfo is None or v.tzinfo.utcoffset(v) is None:
            raise ValueError("%s must be timezone aware" % v)
        return v
    
    @validator("valid_time_end")
    def ensure_none_or_timezone(cls, v):
        if v is None:
            return v
        if v.tzinfo is None or v.tzinfo.utcoffset(v) is None:
            raise ValueError("%s must be timezone aware" % v)
        return v
    
    @validator("valid_geometry")
    def ensure_geometry(cls,v):
        if v is not None and not isinstance(v, Point) and not isinstance(v, Polygon):
            raise ValueError("%s is not a " % v)
    
    def set_value(self, namespace, key, value):
        temp_all_values = json.loads(self.all_values)
        temp_all_values["%s.%s" %(namespace, key)] = value
        self.all_values = json.dumps(temp_all_values)
    
    def get_value(self, namespace, key):
        temp_all_values = json.loads(self.all_values)
        return temp_all_values.get("%s.%s" %(namespace, key))
    
    def set_all_values(self, values_dict):
        self.all_values = json.dumps(values_dict)
        

class ModelConfiguration(BaseModel):
    """All the input data for the model."""
    model_id: constr(min_length=10, max_length=10) = Field(alias="modelId")
    config_parameters: dict = Field(alias="configParameters")

    class Config:
        allow_population_by_field_name = True

    # Can we do this and still serialize the object??
    def get_config_parameter_as_date(self, param_name: str, required=True) -> datetime:
        """
        Input check and typing of date (ISO format)
        """
        param_value = self.config_parameters.get(param_name, None)
        if required and param_value is None:
            raise ModelConfigurationException("%s is required" % param_name)
        try:
            return datetime.fromisoformat(param_value)
        except ValueError:
            raise ModelConfigurationException("%s=%s, which is not a valid date." % (param_name, param_value))

    # Can we do this and still serialize the object??
    def get_config_parameter_as_string(self, param_name: str, required=True) -> datetime:
        """
        Input check
        """
        param_value = self.config_parameters.get(param_name, None)
        if required and param_value is None:
            raise ModelConfigurationException("%s is required" % param_name)
        return param_value
    
    # Can we do this and still serialize the object??
    def get_config_parameter_as_timezone(self, param_name: str, required=True) -> datetime:
        """
        Input check
        """
        param_value = self.config_parameters.get(param_name, None)
        if required and param_value is None:
            raise ModelConfigurationException("%s is required" % param_name)
        
        try:
            return pytz.timezone(param_value)
        except pytz.exceptions.UnknownTimeZoneError:
            raise ModelConfigurationException("%s=%s, which is not a valid timezone." % (param_name, param_value))


class ModelConfigurationException(Exception):
    """
    Should be raised when there's something wrong with the VIPS model configuration
    passed to VIPSModel.set_configuration
    """

class WeatherObservation(BaseModel):
    """Data class for a single weather observation"""
    elementMeasurementTypeId: str
    logIntervalId: int
    timeMeasured: datetime
    value: float

    # Log interval categories
    LOG_INTERVAL_ID_1M: ClassVar[int] = 8
    LOG_INTERVAL_ID_5M: ClassVar[int] = 9
    LOG_INTERVAL_ID_10M: ClassVar[int] = 6
    LOG_INTERVAL_ID_15M: ClassVar[int] = 5
    LOG_INTERVAL_ID_30M: ClassVar[int] = 4
    LOG_INTERVAL_ID_1H: ClassVar[int] = 1
    LOG_INTERVAL_ID_3H: ClassVar[int] = 3
    LOG_INTERVAL_ID_6H: ClassVar[int] = 7
    LOG_INTERVAL_ID_1D: ClassVar[int] = 2

    @validator("timeMeasured")
    def ensure_timezone(cls, v):
        if v.tzinfo is None or v.tzinfo.utcoffset(v) is None:
            raise ValueError("%s must be timezone aware" % v)
        return v

class WeatherElements():

    # Mean temp (Celcius)
    TEMPERATURE_MEAN = "TM"
    # Minimum temperature (Celcius)
    TEMPERATURE_MINIMUM = "TN"
    # Maximum temperature (Celcius)
    TEMPERATURE_MAXIMUM = "TX"
    # Instantaneous temperature (Celcius)
    TEMPERATURE_INSTANTANEOUS = "TT"
    #Soil temperatures at various depths
    SOIL_TEMPERATURE_5CM_MEAN ="TJM5"
    SOIL_TEMPERATURE_10CM_MEAN ="TJM10"
    SOIL_TEMPERATURE_25CM_MEAN ="TJM25"
    SOIL_TEMPERATURE_50CM_MEAN ="TJM50"
    # Dew point temperature (Celcius)
    DEW_POINT_TEMPERATURE = "TD"
    # Aggregated rainfall (millimeters)
    PRECIPITATION = "RR"
    # Average relative humidity (%)
    RELATIVE_HUMIDITY_MEAN = "UM"
    # Instantaneous relative humidity (%)
    RELATIVE_HUMIDITY_INSTANTANEOUS = "UU"
    # Global radiation (W/sqm)
    GLOBAL_RADIATION = "Q0"
    # Immmediate calculated clear sky solar radiation (clear sky)
    GLOBAL_RADIATION_CLEAR_SKY_CALCULATED = "Q0c"
    # Leaf wetness (Minutes per hour)
    LEAF_WETNESS_DURATION = "BT"
    # Leaf wetness at ground level (Minutes per hour)
    LEAF_WETNESS_DURATION_GROUND_LEVEL = "BTg"
    # Soil water content at various depths
    SOIL_WATER_CONTENT_5CM = "VAN5"
    SOIL_WATER_CONTENT_10CM = "VAN10"
    SOIL_WATER_CONTENT_25CM = "VAN25"
    SOIL_WATER_CONTENT_50CM = "VAN50"
    # Soil conductivity at various depths
    SOIL_CONDUCTIVITY_5CM = "LEJ5"
    # Average wind speed (meters / second) for the last 60 minutes, measured at 2 meter height
    WIND_SPEED_2M = "FM2"
    # Average wind speed (meters / second) for the last 60 minutes, measured at 10 meter height
    WIND_SPEED_10M = "FM"
    # Average wind speed (meters / second) for the last 10 minutes, measured at 2 meter height
    WIND_SPEED_10MIN_2M = "FF2"
    # Average wind speed (meters / second) for the last 10 minutes, measured at 10 meter height
    WIND_SPEED_10MIN_10M = "FF"
    # Average wind direction for the last 10 minutes, measured at 2 meter height
    WIND_DIR_10MIN_2M = "DD2"
    # Average wind direction for the last 10 minutes, measured at 10 meter height
    WIND_DIR_10MIN_10M = "DD"
    # Potential evapotranspiration by the Penman equation
    POTENTIAL_EVAPORATION = "EPP"