import json
import boto3
import numpy as np
from http import HTTPStatus
from random import randint
from flask import current_app

import inject
from keras.models import Sequential
import tensorflow as tf

from app.extensions.utils.enum.aws_enum import (
    AwsServiceEnum,
)
from core.domains.iot.enum.iot_enum import HumidityStatusEnum
from app.extensions.push.notification import FCM
from core.domains.iot.dto.iot_dto import CreateReceiveDataDto, FailureReceiveDataDto
from core.domains.iot.repository.iot_repository import IOTRepository
from core.domains.notification.dto.notification_dto import CreateNotificationDto
from core.domains.notification.enum.notification_enum import NotificationTopicEnum, NotificationStatusEnum
from core.domains.notification.repository.notification_repository import NotificationRepository
from core.domains.plant.enum.plant_enum import TemperatureApprValueEnum, TemperatureApprEventEnum, \
    IndoorHumidityApprValueEnum, IndoorHumidityApprEventEnum, SoilHumidityApprValueEnum, SoilHumidityApprEventEnum, \
    IlluminanceApprValueEnum, IlluminanceApprEventEnum
from core.domains.user.repository.user_repository import UserRepository
from core.use_case_output import UseCaseSuccessOutput, UseCaseFailureOutput, FailureType
from app.persistence.model.plant_info_model import PlantInfoModel


class IOTBaseUseCase:
    @inject.autoparams()
    def __init__(self, iot_repo: IOTRepository, user_repo: UserRepository, notification_repo: NotificationRepository):
        self._iot_repo = iot_repo
        self._user_repo = user_repo
        self._notification_repo = notification_repo


class CreateReceiveDataUseCase(IOTBaseUseCase):
    def execute(
        self, dto: CreateReceiveDataDto
    ) -> UseCaseSuccessOutput | UseCaseFailureOutput:
        plant_dict: dict | None = self._user_repo.get_plant_id_by_mac(sensormac=dto.sensormac)
        dto.plant_id = plant_dict.get("plant_id")
        self._iot_repo.create_receive_data(dto=dto)

        data_feeds: list[PlantInfoModel] = self._iot_repo.get_predict_data(
            plant_id=dto.plant_id,
        )
        is_high_humidity: int = self.get_is_high_humidity(data_feeds=data_feeds)

        if plant_dict.get("fcm_token"):
            # 인큐베이터 push
            # push message
            body = None
         #   if is_high_humidity != HumidityStatusEnum.NORMAL.value:
          #      body: str | None = self._get_humidity_message(
           #         plant_nickname=plant_dict.get("plant_nickname"),
            #        is_high_humidity=is_high_humidity
             #   )
            if dto.interrupt == "PUSH1":
                body: str = self._get_plant_status(dto=dto, plant_nickname=plant_dict.get("plant_nickname"))
            elif dto.interrupt == "PUSH2":
                body: str = self._get_afternoon_plant_message(plant_nickname=plant_dict.get("plant_nickname"))
            elif dto.interrupt == "PUSH3":
                body: str = self._get_night_plant_message(plant_nickname=plant_dict.get("plant_nickname"))
            elif dto.interrupt == "PUSH4":
                body: str = "배터리가 부족합니다"

            if body:
                data = dict(
                    user_id=plant_dict.get("user_id"),
                    topic=NotificationTopicEnum.INCUBATOR.value,
                )
                # push notification
                create_notification_dto: CreateNotificationDto = self._push_notification(
                    user_id=plant_dict.get("user_id"),
                    title=None,
                    body=body,
                    token=plant_dict.get("fcm_token"),
                    topic=str(NotificationTopicEnum.INCUBATOR.value),
                    data=data
                )
                # notification history 저장
                self._notification_repo.create_notification(dto=create_notification_dto)

        return UseCaseSuccessOutput()

    def get_is_high_humidity(self, data_feeds: list[PlantInfoModel] | None) -> int:
        """
        1. 전처리
        2. 습도 예측
        3. 푸시알림을 보낼지 안보낼지 판단

        return:
        """
        dataset: np.numpy = self._transfer_data(data_feeds=data_feeds)
        predicted_data = None

        for i in range(3):
            try:
                predict_model: Sequential = self._get_model_from_s3()
                predicted_data = predict_model.predict(dataset)
                break
            except Exception as e:
                pass

        humidities = predicted_data[0, :24]
        soile_humidities = predicted_data[0, 24:]

        return_humidity = HumidityStatusEnum.NORMAL.value
        for i in range(len(humidities)):
            if soile_humidities[i] >= 90 and humidities[i] >= 80:
                return_humidity = HumidityStatusEnum.HIGH.value
            elif soile_humidities[i] >= 30:
                return_humidity = HumidityStatusEnum.LOW.value
        return return_humidity

    def _get_model_from_s3(self) -> Sequential:
        file_path = "model.h5"
        client = boto3.client(
            service_name=AwsServiceEnum.S3.value,
            region_name=current_app.config.get("AWS_REGION_NAME"),
            aws_access_key_id=current_app.config.get("AWS_ACCESS_KEY"),
            aws_secret_access_key=current_app.config.get("AWS_SECRET_ACCESS_KEY"),
        )
        client.download_file('plantra-groot-bucket', 'ai_model/LSTM_PLANTRA.h5', file_path)

        predict_model: Sequential = tf.keras.models.load_model(file_path)
        return predict_model

    def _transfer_data(self, data_feeds: list[PlantInfoModel]) -> np.array:
        """
        데이터 전처리
        """
        all_time_data = list()
        for data_feed in data_feeds:
            one_time_data = [
                float(data_feed.temperature) if not np.isnan(float(data_feed.temperature)) else 0,
                float(data_feed.indoor_humidity) if not np.isnan(float(data_feed.indoor_humidity)) else 0,
                float(data_feed.illuminance) if not np.isnan(float(data_feed.illuminance)) else 0,
                float(data_feed.soil_humidity) if not np.isnan(float(data_feed.soil_humidity)) else 0,
            ]
            all_time_data.append(one_time_data)

        dataset = np.array(all_time_data)

        # 시계열 데이터가 120개가 안되는경우 0으로 padding
        zeros = np.zeros((720, 4))
        dataset = np.vstack([zeros, dataset])
        dataset = dataset[-720:]
        dataset = dataset[range(0, 720, 6)]
        dataset = dataset.reshape((1, 120, 4))
        return dataset

    def _push_notification(
        self, user_id: int, title: str | None, body: str, token: str, topic: str, data: dict
    ) -> CreateNotificationDto:
        if FCM.send_message(title=title, body=body, token=token, data=data):
            status = NotificationStatusEnum.SUCCESS.value
        else:
            status = NotificationStatusEnum.FAILURE.value

        return CreateNotificationDto(
            user_id=user_id,
            token=token,
            topic=topic,
            status=status,
            message=json.dumps(dict(title=title, body=body), ensure_ascii=False),
        )

    def _get_plant_status(self, dto: CreateReceiveDataDto, plant_nickname: str) -> str:
        div_data, plant_status_2 = None, None
        div = randint(1, 4)
        match div:
            case 1:
                div_data = dto.temperature
                if TemperatureApprValueEnum.HIGH.value <= div_data:  # high
                    plant_status_2 = TemperatureApprEventEnum.HIGH.value
                elif TemperatureApprValueEnum.LOW.value >= div_data:  # low
                    plant_status_2 = TemperatureApprEventEnum.LOW.value
                else:  # 적정수치
                    plant_status_2 = TemperatureApprEventEnum.APPR.value

            case 2:
                div_data = dto.indoor_humidity
                if IndoorHumidityApprValueEnum.HIGH.value <= div_data:  # high
                    plant_status_2 = IndoorHumidityApprEventEnum.HIGH.value
                elif IndoorHumidityApprValueEnum.LOW.value >= div_data:  # low
                    plant_status_2 = IndoorHumidityApprEventEnum.LOW.value
                else:  # 적정수치
                    plant_status_2 = IndoorHumidityApprEventEnum.APPR.value

            case 3:
                div_data = dto.soil_humidity
                if SoilHumidityApprValueEnum.HIGH.value <= div_data:  # high
                    plant_status_2 = SoilHumidityApprEventEnum.HIGH.value
                elif SoilHumidityApprValueEnum.LOW.value >= div_data:  # low
                    plant_status_2 = SoilHumidityApprEventEnum.LOW.value
                else:  # 적정수치
                    plant_status_2 = SoilHumidityApprEventEnum.APPR.value

            case 4:
                div_data = dto.illuminance
                if IlluminanceApprValueEnum.HIGH.value <= div_data:  # high
                    plant_status_2 = IlluminanceApprEventEnum.HIGH.value
                elif IlluminanceApprValueEnum.LOW.value >= div_data:  # low
                    plant_status_2 = IlluminanceApprEventEnum.LOW.value
                else:  # 적정수치
                    plant_status_2 = IlluminanceApprEventEnum.APPR.value
            case _:  # default
                pass

        return f"{plant_nickname}{plant_status_2}"

    def _get_afternoon_plant_message(self, plant_nickname: str) -> str:
        div = randint(1, 5)
        match div:
            case 1:
               return f"{plant_nickname}에게 변화가 생기고 있지 않나요?"

            case 2:
                return f"{plant_nickname} : 날이 더워져요! 벌레 조심해주세요."
            
            case 3:
                return f"{plant_nickname} : 식집사 여러분 더위 조심하세요!"
            
            case 4:
                return f"{plant_nickname} : 저를 사랑하고 챙겨줘서 고마워요."
            
            case 5:
                return f"{plant_nickname} : 제가 커진거같으면 분갈이를 해주세요! 좀 커졌나요?"
            

            case _:  # default
                pass

    def _get_night_plant_message(self, plant_nickname: str) -> str:
        div = randint(1, 5)
        match div:
            case 1:
                return f"{plant_nickname}에게 더 많은 관심을 줬으면 해요."

            case 2:
                return f"{plant_nickname} : 2024년도 건강하게 예쁘게 돌봐주세요!"
            
            case 3:
                return f"{plant_nickname} : 다른 친구들도 생겼으면 좋겠어요!"
            
            case 4:
                return f"{plant_nickname} : 오늘도 고생 많았어요."
            
            case 5:
                return f"{plant_nickname} : 너무 뜨거운 햇살은 목이 마를 수 있답니다."
        
    

            case _:  # default
                pass

    def _get_humidity_message(self,
                              plant_nickname: str,
                              is_high_humidity: int
                              ) -> str | None:
        if is_high_humidity == HumidityStatusEnum.HIGH.value:
            return f"{plant_nickname}는(은) 지금 과습 위험이 있어요. 실내 환기를 해주고 안정될 때 까지 물을 주지 마세요!."
        elif is_high_humidity == HumidityStatusEnum.LOW.value:
            return f"{plant_nickname}는(은) 지금 몹시 목이 말라요. 흠뻑 젖을 때 까지 물을 주세요."
        else:
            return None


class FailureReceiveDataUseCase(IOTBaseUseCase):
    def execute(self, dto: FailureReceiveDataDto) -> UseCaseFailureOutput:
        self._iot_repo.create_failure_receive_data(dto=dto)
        return UseCaseFailureOutput(
            type="failure receive data",
            message=FailureType.INTERNAL_ERROR,
            code=HTTPStatus.INTERNAL_SERVER_ERROR,
        )
