"""Rules to handle sun sensors."""
import logging

import HABApp

import habapp_rules.common.config.filter
import habapp_rules.common.filter
import habapp_rules.common.hysteresis
import habapp_rules.core.helper
import habapp_rules.core.logger
import habapp_rules.core.state_machine_rule
import habapp_rules.sensors.config.sun

LOGGER = logging.getLogger(__name__)


class _SensorBase(HABApp.Rule):
	"""Base class for sun sensors."""

	def __init__(self, config: habapp_rules.sensors.config.sun.BrightnessConfig | habapp_rules.sensors.config.sun.TemperatureDifferenceConfig, item_input: HABApp.openhab.items.NumberItem) -> None:
		"""Init of base class for sun sensors.

		:param config: config for sun sensor
		:param item_input: item for input value (brightness or temperature difference)
		"""
		self._config = config

		# init HABApp Rule
		HABApp.Rule.__init__(self)
		self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.output.name)

		# init exponential filter
		name_input_exponential_filtered = f"H_{item_input.name.removeprefix('H_')}_filtered"
		habapp_rules.core.helper.create_additional_item(name_input_exponential_filtered, "Number", name_input_exponential_filtered.replace("_", " "), config.parameter.filtered_signal_groups)
		item_input_filtered = HABApp.openhab.items.NumberItem.get_item(name_input_exponential_filtered)

		exponential_filter_config = habapp_rules.common.config.filter.ExponentialFilterConfig(
			items=habapp_rules.common.config.filter.ExponentialFilterItems(
				raw=item_input,
				filtered=item_input_filtered),
			parameter=habapp_rules.common.config.filter.ExponentialFilterParameter(
				tau=config.parameter.filter_tau,
				instant_increase=config.parameter.filter_instant_increase,
				instant_decrease=config.parameter.filter_instant_decrease
			)
		)
		habapp_rules.common.filter.ExponentialFilter(exponential_filter_config)

		# attributes
		self._hysteresis_switch = habapp_rules.common.hysteresis.HysteresisSwitch(config.threshold, config.parameter.hysteresis, False)

		# callbacks
		item_input_filtered.listen_event(self._cb_input_filtered, HABApp.openhab.events.ItemStateChangedEventFilter())
		if config.items.threshold is not None:
			config.items.threshold.listen_event(self._cb_threshold, HABApp.openhab.events.ItemStateChangedEventFilter())

	def _send_output(self, new_value: str) -> None:
		"""Send output if different.

		:param new_value: new value which should be sent
		"""
		if new_value != self._config.items.output.value:
			self._config.items.output.oh_send_command(new_value)
			self._instance_logger.debug(f"Set output '{self._config.items.output.name}' to {new_value}")

	def _cb_input_filtered(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
		"""Callback, which is triggered if the filtered input value changed.

		:param event: trigger event
		"""
		value = self._hysteresis_switch.get_output(event.value)
		self._send_output(value)

	def _cb_threshold(self, event: HABApp.openhab.events.ItemStateChangedEvent) -> None:
		"""Callback, which is triggered if the threshold changed.

		:param event: trigger event
		"""
		self._hysteresis_switch.set_threshold_on(event.value)


class SensorBrightness(_SensorBase):
	"""Rules class to set sun protection depending on brightness level.

	# Items:
	Number    brightness                    "Current brightness [%d lux]"       {channel="..."}
	Number    brightness_threshold          "Brightness threshold [%d lux]"
	Switch    sun_protection_brightness     "Sun protection brightness [%s]     {channel="..."}

	# Config:
	config = habapp_rules.sensors.config.sun.BrightnessConfig(
		items=habapp_rules.sensors.config.sun.BrightnessItems(
			brightness="brightness",
			output="sun_protection_brightness",
			brightness_threshold="brightness_threshold"
		)
	)

	# Rule init:
	habapp_rules.sensors.sun.SensorBrightness(config)
	"""

	def __init__(self, config: habapp_rules.sensors.config.sun.BrightnessConfig) -> None:
		"""Init of sun sensor which takes a brightness value

		:param config: config for the sun sensor which is using brightness
		"""
		_SensorBase.__init__(self, config, config.items.brightness)


class SensorTemperatureDifference(_SensorBase):
	"""Rules class to set sun protection depending on temperature difference. E.g. temperature in the sun / temperature in the shadow.

	# Items:
	Number    temperature_sun               "Temperature sun [%.1f °C]"         {channel="..."}
	Number    temperature_shadow            "Temperature shadow [%.1f °C]"      {channel="..."}
	Number    temperature_threshold         "Temperature threshold [%.1f °C]"
	Switch    sun_protection_temperature    "Sun protection temperature [%s]    {channel="..."}

	# Config:
	config = habapp_rules.sensors.config.sun.TemperatureDifferenceConfig(
		items=habapp_rules.sensors.config.sun.TemperatureDifferenceItems(
			temperatures=["temperature_sun", "temperature_shadow"],
			output="sun_protection_temperature",
			threshold="temperature_threshold"
		)
	)

	# Rule init:
	habapp_rules.sensors.sun.SensorTempDiff(config)
	"""

	def __init__(self, config: habapp_rules.sensors.config.sun.TemperatureDifferenceConfig) -> None:
		"""Init of sun sensor which takes a two or more temperature values (one in the sun and one in the shadow)

		:param config: config for the sun sensor which is using temperature items
		"""
		self._config = config
		name_temperature_diff = f"H_Temperature_diff_for_{config.items.output.name}"
		habapp_rules.core.helper.create_additional_item(name_temperature_diff, "Number", name_temperature_diff.replace("_", " "))
		self._item_temp_diff = HABApp.openhab.items.NumberItem.get_item(name_temperature_diff)

		_SensorBase.__init__(self, config, self._item_temp_diff)

		# callbacks
		for temperature_item in self._config.items.temperatures:
			temperature_item.listen_event(self._cb_temperature, HABApp.openhab.events.ItemStateChangedEventFilter())

		# calculate temperature difference
		self._cb_temperature(None)

	def _cb_temperature(self, _: HABApp.openhab.events.ItemStateChangedEvent | None):
		"""Callback, which is triggered if a temperature value changed."""
		filtered_items = [itm for itm in habapp_rules.core.helper.filter_updated_items(self._config.items.temperatures, self._config.parameter.ignore_old_values_time) if itm.value is not None]
		if len(filtered_items) < 2:
			return
		value_min = min(item.value for item in filtered_items)
		value_max = max(item.value for item in filtered_items)

		self._item_temp_diff.oh_send_command(value_max - value_min)


class SunPositionFilter(HABApp.Rule):
	"""Rules class to filter a switch state depending on the sun position. This can be used to only close the blinds of a window, if the sun hits the window

	# Items:
	Number    sun_azimuth           "Sun Azimuth [%.1f °]"              {channel="astro..."}
	Number    sun_elevation         "Sun Elevation [%.1f °]"            {channel="astro..."}
	Switch    sun_shining           "Sun is shining [%s]
	Switch    sun_hits_window       "Sun hits window [%s]

	# Config:
	config = habapp_rules.sensors.config.sun.SunPositionConfig(
		items=habapp_rules.sensors.config.sun.SunPositionItems(
			azimuth="sun_azimuth",
			elevation="sun_elevation",
			input="sun_shining",
			output="sun_hits_window"
		),
		parameter=habapp_rules.sensors.config.sun.SunPositionParameter(
			sun_position_window=habapp_rules.sensors.config.sun.SunPositionWindow(40, 120)
		)
	)

	# Rule init:

	habapp_rules.sensors.sun.SunPositionFilter(config)
	"""

	def __init__(self, config: habapp_rules.sensors.config.sun.SunPositionConfig) -> None:
		"""Init of sun position filter.

		:param config: config for the sun position filter
		"""
		self._config = config

		# init HABApp Rule
		HABApp.Rule.__init__(self)
		self._instance_logger = habapp_rules.core.logger.InstanceLogger(LOGGER, config.items.output.name)

		# callbacks
		config.items.azimuth.listen_event(self._update_output, HABApp.openhab.events.ItemStateChangedEventFilter())  # listen_event for elevation is not needed because elevation and azimuth is updated together
		config.items.input.listen_event(self._update_output, HABApp.openhab.events.ItemStateChangedEventFilter())

		self._update_output(None)

	def _sun_in_window(self, azimuth: float, elevation: float) -> bool:
		"""Check if the sun is in the 'sun window' where it hits the target.

		:param azimuth: azimuth of the sun
		:param elevation: elevation of the sun
		:return: True if the sun hits the target, else False
		"""
		sun_in_window = False

		if any(window.azimuth_min <= azimuth <= window.azimuth_max and window.elevation_min <= elevation <= window.elevation_max for window in self._config.parameter.sun_position_windows):
			sun_in_window = True

		return sun_in_window

	def _update_output(self, _: HABApp.openhab.events.ItemStateChangedEvent | None) -> None:
		"""Callback, which is triggered if the sun position or input changed."""
		azimuth = self._config.items.azimuth.value
		elevation = self._config.items.elevation.value

		if azimuth is None or elevation is None:
			self._instance_logger.warning(f"Azimuth or elevation is None -> will set output to input. azimuth = {azimuth} | elevation = {elevation}")
			filter_output = self._config.items.input.value
		elif self._config.items.input.value in {"OFF", None}:
			filter_output = "OFF"
		else:
			filter_output = "ON" if self._sun_in_window(azimuth, elevation) else "OFF"

		if filter_output != self._config.items.output.value:
			self._config.items.output.oh_send_command(filter_output)
