import secrets
from datetime import datetime, timedelta, timezone
from .states import AlarmState, AlarmAttrs
from .trigger import Trigger, TriggerType
from ..tags.tag import Tag, MachineObserver
from ..tags.cvt import CVTEngine
from ..modules.users.users import User
from ..utils.decorators import validate_types, logging_error_handler, set_event, put_alarm_state
from ..models import FloatType, IntegerType, StringType
from ..variables import *
from statemachine import State, StateMachine
from flask_socketio import SocketIO


class Alarm(StateMachine):
    r"""
    Represents an Alarm entity with state machine logic.

    Implements the standard alarm lifecycle defined in ISA 18.2, including states like
    Normal, Unacknowledged, Acknowledged, Shelved, and Suppressed.

    It monitors a `Tag` value against a `Trigger` condition and transitions states accordingly.
    """

    # MAIN STATES
    normal = State("normal", initial=True)
    unack_alarm = State("unack_alarm")
    ack_alarm = State("ack_alarm")
    rtn_unack = State("rtn_unack")

    # OUT OF SERVICE STATES
    shelved = State("shelved")
    suppressed_by_design = State("suppressed_by_design")
    out_of_service = State("out_of_service")

    # MAIN TRANSITIONS
    normal_to_unack_alarm = normal.to(unack_alarm)
    unack_alarm_to_ack_alarm = unack_alarm.to(ack_alarm)
    ack_alarm_to_normal = ack_alarm.to(normal)
    unack_alarm_to_rtn_unack = unack_alarm.to(rtn_unack)
    rtn_unack_to_normal = rtn_unack.to(normal)
    rtn_unack_to_unack_alarm = rtn_unack.to(unack_alarm)

    # SHELVED TRANSITIONS
    normal_to_shelved = normal.to(shelved)
    unack_alarm_to_shelved = unack_alarm.to(shelved)
    ack_alarm_to_shelved = ack_alarm.to(shelved)
    rtn_unack_to_shelved = rtn_unack.to(shelved)
    shelved_to_normal = shelved.to(normal)
    shelved_to_unack_alarm = shelved.to(unack_alarm)

    # SUPPRESSED BY DESIGN TRANSITIONS
    normal_to_suppressed_by_design = normal.to(suppressed_by_design)
    unack_alarm_to_suppressed_by_design = unack_alarm.to(suppressed_by_design)
    ack_alarm_to_suppressed_by_design = ack_alarm.to(suppressed_by_design)
    rtn_unack_to_suppressed_by_design = rtn_unack.to(suppressed_by_design)
    suppressed_by_design_to_normal = suppressed_by_design.to(normal)
    suppressed_by_design_to_unack_alarm = suppressed_by_design.to(unack_alarm)


    # OUT OF SERVICE TRANSITIONS
    normal_to_out_of_service = normal.to(out_of_service)
    unack_alarm_to_out_of_service = unack_alarm.to(out_of_service)
    ack_alarm_to_out_of_service = ack_alarm.to(out_of_service)
    rtn_unack_to_out_of_service = rtn_unack.to(out_of_service)
    out_of_service_to_normal = out_of_service.to(normal)
    out_of_service_to_unack_alarm = out_of_service.to(unack_alarm)

    def __init__(
            self,
            name:str, 
            tag:Tag,
            alarm_type:StringType,
            alarm_setpoint:IntegerType|FloatType,
            description:str="",
            state:str=None,
            timestamp:datetime=None,
            ack_timestamp:datetime=None,
            alarm_deadband:IntegerType|FloatType=FloatType(0.0),
            alarm_on_delay:IntegerType|FloatType=FloatType(0.0),
            alarm_off_delay:IntegerType|FloatType=FloatType(0.0),
            identifier:str=None,
            user:User=None,
            reload:bool=False
        ):
        r"""
        Initializes the Alarm.

        **Parameters:**

        * **name** (str): Alarm name.
        * **tag** (Tag): The tag being monitored.
        * **alarm_type** (StringType): Trigger type (HI, LO, etc.).
        * **alarm_setpoint** (IntegerType|FloatType): The limit value.
        * **description** (str): Alarm description.
        * **state** (str, optional): Initial state.
        * **identifier** (str, optional): Unique ID.
        """
        from ..logger.alarms import AlarmsLoggerEngine
        self.alarm_engine = AlarmsLoggerEngine()
        self.tag_engine = CVTEngine()
        self.name = name
        self.tag = tag
        self.segment = tag.segment
        self.manufacturer = tag.manufacturer
        self.attach(machine=self, tag=tag)
        self.description = description        
        self.alarm_setpoint = Trigger()
        self.alarm_setpoint.value = alarm_setpoint.value
        self.alarm_setpoint.type = TriggerType(value=alarm_type.value.upper())
        alarm_deadband.unit = tag.get_display_unit()
        self.alarm_deadband = alarm_deadband
        self.alarm_on_delay = alarm_on_delay
        self.alarm_off_delay = alarm_off_delay
        self.timestamp = timestamp 
        self.ack_timestamp = ack_timestamp
        self.state = AlarmState.NORM
        if state:
            
            for _, attr in AlarmState.__dict__.items():
                
                if isinstance(attr, AlarmAttrs):
                    
                    if state==attr.state:
                        
                        self.state = attr
                        break
        if identifier:
            self.identifier = identifier
        else:
            self.identifier = secrets.token_hex(4)

        self._shelved_time:datetime = None
        self._shelved_until:datetime = None
        self._shelved_options_time = {
            'days': 0,
            'seconds': 0,
            'microseconds': 0,
            'milliseconds': 0,
            'minutes': 0,
            'hours': 0,
            'weeks': 0
        }
        transitions = []
        for state in self.states:
            transitions.extend(state.transitions)
        self.transitions = transitions
        self.sio:SocketIO|None = None
        super(Alarm, self).__init__()

    @logging_error_handler
    @put_alarm_state
    def on_enter_normal(self):
        self.state = AlarmState.NORM
        self.timestamp = None 
        self.ack_timestamp = None

    @logging_error_handler
    @put_alarm_state
    def on_enter_unack_alarm(self):

        self.state = AlarmState.UNACK
        self.timestamp = self.__timestamp
        self.alarm_engine.create_record_on_alarm_summary(
                name=self.name, 
                state=self.state.state, 
                timestamp=self.timestamp,
                ack_timestamp=self.ack_timestamp
            )

    @logging_error_handler
    @put_alarm_state
    def on_enter_ack_alarm(self):

        self.state = AlarmState.ACKED
        self.ack_timestamp = self.__timestamp
        self.alarm_engine.put_record_on_alarm_summary(
            name=self.name, 
            state=self.state.state, 
            ack_timestamp=self.ack_timestamp
        )
    
    @logging_error_handler
    @put_alarm_state
    def on_enter_rtn_unack(self):
        
        self.timestamp = None
        self.state = AlarmState.RTNUN
        self.alarm_engine.put_record_on_alarm_summary(
            name=self.name, 
            state=self.state.state
        )
    
    @logging_error_handler
    @put_alarm_state
    def on_enter_shelved(self):
        
        self.state = AlarmState.SHLVD

    @logging_error_handler
    @put_alarm_state
    def on_enter_suppressed_by_design(self):
        
        self.state = AlarmState.DSUPR

    @logging_error_handler
    @put_alarm_state
    def on_enter_out_of_service(self):
        
        self.state = AlarmState.OOSRV

    def set_socketio(self, sio:SocketIO):
        r"""
        Sets the SocketIO instance for real-time updates.

        **Parameters:**

        * **sio** (SocketIO): The SocketIO server instance.
        """
        self.sio:SocketIO = sio

    @logging_error_handler
    @validate_types(
            tag=str, 
            value=Temperature|Length|Current|Time|Pressure|Mass|Force|Power|VolumetricFlow|MassFlow|Density|Percentage|Adimentional, 
            timestamp=datetime, 
            output=None)
    def notify(
        self, 
        tag:str, 
        value:Temperature|Length|Current|Time|Pressure|Mass|Force|Power|VolumetricFlow|MassFlow|Density|Percentage|Adimentional, 
        timestamp:datetime):
        r"""
        Callback triggered when the monitored Tag value changes (Observer pattern).
        
        It evaluates the new value against alarm logic and triggers state transitions.

        **Parameters:**

        * **tag** (str): Tag name.
        * **value** (Quantity): The new tag value.
        * **timestamp** (datetime): Time of the value change.
        """ 
        self.__timestamp = timestamp
        if self.state not in (AlarmState.DSUPR, AlarmState.SHLVD, AlarmState.OOSRV):     
            if self.alarm_setpoint.type in (TriggerType.HH, TriggerType.H):

                if value.value > self.alarm_setpoint.value:

                    self.abnormal_condition()
                
                else: 
                    self.normal_condition()

            elif self.alarm_setpoint.type in (TriggerType.L, TriggerType.LL):

                if value.value < self.alarm_setpoint.value:

                    self.abnormal_condition()

                else:

                    self.normal_condition()

            else: # Boolean Alarm
                
                if value.value == bool(self.alarm_setpoint.value):

                    self.abnormal_condition()

                else:

                    self.normal_condition()

        if self.state==AlarmState.SHLVD:

            if datetime.now(timezone.utc) >= self._shelved_until:

                self.unshelve()
        
    @logging_error_handler
    def abnormal_condition(self):
        r"""
        Triggers transition to an alarm state (abnormal).
        """
        current_state = self.current_state.name.lower()
        transition_name = f'{current_state}_to_unack_alarm'
        self.__transition(transition_name=transition_name)

    @logging_error_handler
    def normal_condition(self):
        r"""
        Triggers transition to normal or return-to-normal states.
        """
        current_state = self.current_state.name.lower()

        if current_state=="unack_alarm":

            transition_name = f'{current_state}_to_rtn_unack'
            self.__transition(transition_name=transition_name)

        elif current_state=="ack_alarm":

            transition_name = f'{current_state}_to_normal'
            self.__transition(transition_name=transition_name)

    @logging_error_handler
    @set_event(message=f"Acknowledged", classification="Alarm", priority=2, criticity=3)
    def acknowledge(self, user:User=None):
        r"""
        Acknowledges the alarm.

        **Parameters:**

        * **user** (User, optional): User performing the acknowledgment.
        """
        current_state = self.current_state.name.lower()

        if current_state=="unack_alarm":

            transition_name = f'{current_state}_to_ack_alarm'

        elif current_state=="rtn_unack":

            transition_name = f'{current_state}_to_normal'


        tag = self.tag_engine.get_tag_by_name(name=self.tag.name)
        self.alarm_engine.put_record_on_alarm_summary(
            name=self.name, 
            ack_timestamp=tag.get_timestamp()
        )
        self.__transition(transition_name=transition_name)
        return self, f"{self.tag.get_name()}"

    @logging_error_handler
    @set_event(message=f"Shelved", classification="Alarm", priority=2, criticity=3)
    def shelve(self, user:User=None, **options):
        r"""
        Temporarily suppresses the alarm (Shelving).

        **Parameters:**

        * **options**: Time duration arguments (days, hours, minutes, seconds).
        """
        options_time = {key: options[key] if key in options else self._shelved_options_time[key] for key in self._shelved_options_time}
        
        if options_time!=self._shelved_options_time:
            
            self._shelved_time = datetime.now(timezone.utc)
            self._shelved_until = self._shelved_time + timedelta(**options_time)

        current_state = self.current_state.name.lower()
        transition_name = f'{current_state}_to_shelved'
        self.__transition(transition_name=transition_name)
        return self, f"{self.tag.get_name()}"

    @logging_error_handler
    @set_event(message=f"Unshelved", classification="Alarm", priority=2, criticity=3)
    def unshelve(self, user:User=None):
        r"""
        Manually un-shelves the alarm, returning it to service.
        """
        self.__return_to_service()
        return self, f"{self.tag.get_name()}"

    @logging_error_handler
    @set_event(message=f"Designed suppression", classification="Alarm", priority=2, criticity=3)
    def designed_suppression(self, user:User=None):
        r"""
        Suppresses the alarm by design (e.g., maintenance mode).
        """
        current_state = self.current_state.name.lower()
        transition_name = f'{current_state}_to_suppressed_by_design'
        self.__transition(transition_name=transition_name)
        return self, f"{self.tag.get_name()}"

    @logging_error_handler
    @set_event(message=f"Designed unsuppression", classification="Alarm", priority=2, criticity=3)
    def designed_unsuppression(self, user:User=None):
        r"""
        Removes designed suppression.
        """
        self.__return_to_service()
        return self, f"{self.tag.get_name()}"

    @logging_error_handler
    @set_event(message=f"Removed from service", classification="Alarm", priority=2, criticity=3)
    def remove_from_service(self, user:User=None):
        r"""
        Takes the alarm out of service entirely.
        """
        current_state = self.current_state.name.lower()
        transition_name = f'{current_state}_to_out_of_service'
        self.__transition(transition_name=transition_name)
        return self, f"{self.tag.get_name()}"

    @logging_error_handler
    @set_event(message=f"Returned to service", classification="Alarm", priority=2, criticity=3)
    def return_to_service(self, user:User=None):
        r"""
        Returns the alarm to service from 'Out of Service'.
        """
        self.__return_to_service()

        return self, f"{self.tag.get_name()}"

    @logging_error_handler
    def attach(self, machine, tag:Tag):
        
        def attach_observer(machine, tag:Tag):

            observer = MachineObserver(machine)
            query = dict()
            query["action"] = "attach_observer"
            query["parameters"] = {
                "name": tag.name,
                "observer": observer,
            }
            self.tag_engine.request(query)
            self.tag_engine.response()

        attach_observer(machine, tag)

    @set_event(message=f"Updated", classification="Alarm", priority=2, criticity=3)
    def put(
            self, 
            user:User=None,
            name:str=None,
            tag:str=None,
            description:str=None,
            alarm_type:TriggerType=None,
            trigger_value:float=None):
        r"""
        Updates the alarm configuration.

        **Parameters:**
        
        * **name** (str): Alarm name.
        * **tag** (str): Tag bound to alarm.
        * **description** (str): Alarm description.
        * **alarm_type** (TriggerType): Alarm type ['HIGH-HIGH', 'HIGH', 'LOW', 'LOW-LOW', 'BOOL'].
        * **trigger_value** (float): Alarm trigger value.

        **Returns:**

        * **tuple**: (Alarm instance, status message)
        """
        message = ""
        if alarm_type:

            if alarm_type.value.upper() in ["HIGH-HIGH", "HIGH", "LOW", "LOW-LOW", "BOOL"]:

                self.alarm_setpoint.type = alarm_type

                message += f" alarm_type: {alarm_type.value}"

        if trigger_value:
            
            self.alarm_setpoint.value = float(trigger_value)
            message += f" trigger value: {trigger_value}"
        
        if name:

            self._name = name
            message += f" name: {name}"

        if tag:

            self._tag = tag
            message += f" tag: {tag}"

        if description:

            self._description = description
            message += f" description: {description}"
        
        return self, message

    def _get_active_transitions(self):
        r"""
        Gets allowed transitions based on the current state.

        **Returns:**

        * **list**: List of available transitions.
        """
        result = list()

        current_state = self.current_state
        transitions = self.transitions

        for transition in transitions:

            if transition.source == current_state:

                result.append(transition)

        return result
    
    @logging_error_handler
    def __transition(self, transition_name:str):

        allowed_transitions = self._get_active_transitions()
        for _transition in allowed_transitions:
            
            if f"{_transition.source.name}_to_{_transition.target.name}"==transition_name:
                
                self.send(transition_name)

    @logging_error_handler
    def __return_to_service(self):

        current_state = self.current_state.name.lower()

        if self.state.alarm_status.lower()=="active":

            transition_name = f'{current_state}_to_unack_alarm'

        else:

            transition_name = f'{current_state}_to_normal'
        
        self.__transition(transition_name=transition_name)

    @logging_error_handler
    def get_operator_actions(self)->list:
        r"""
        Returns a list of available actions for the operator based on current state.

        **Returns:**

        * **dict**: Map of Action Name -> Action Method.
        """
        current_state = self.current_state.name.lower()
            
        if current_state in ("unack_alarm", "rtn_unack"):

            result = {
                "Acknowledge": "acknowledge",
                "Shelve": "shelve",
                "Designed Suppression": "designed_suppression",
                "Remove From Service": "remove_from_service"
            }

        elif current_state=="suppressed_by_design":

            result = {
                "Designed Unsuppression": "designed_unsuppression"
            }

        elif current_state=="out_of_service":

            result = {
                "Return To Service": "return_to_service"
            }

        else:

            result = {
                "Shelve": "shelve",
                "Designed Suppression": "designed_suppression",
                "Remove From Service": "remove_from_service"
            }

        return result

    def serialize(self):
        r"""
        Serializes the alarm object to a JSON-compatible dictionary.

        **Returns:**

        * **dict**: Alarm data including state, setpoint, and metadata.
        """
        timestamp = self.timestamp
        if timestamp:

            timestamp = timestamp.strftime(self.tag_engine.DATETIME_FORMAT)

        ack_timestamp = self.ack_timestamp
        if ack_timestamp:

            ack_timestamp = ack_timestamp.strftime(self.tag_engine.DATETIME_FORMAT)

        return {
            "identifier": self.identifier,
            "segment": self.segment,
            "manufacturer": self.manufacturer,
            "timestamp": timestamp,
            "name": self.name,
            "tag": self.tag.name,
            "state": self.state.serialize(),
            "alarm_setpoint": self.alarm_setpoint.serialize(),
            "ack_timestamp": ack_timestamp,
            "description": self.description,
            "actions": self.get_operator_actions()
        }
