"""Rule Engine models for IoT automation.

This module contains models for the automation rule engine:
- Rule: Automation rule definitions
- RuleExecution: Audit log for rule execution attempts
"""

from datetime import datetime
from enum import Enum as PyEnum

from sqlalchemy import (
    Boolean,
    CheckConstraint,
    Column,
    DateTime,
    ForeignKey,
    Index,
    Integer,
    Numeric,
    Text,
    text,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import relationship

from iot_db.models.base import Base


class ConditionOperator(PyEnum):
    """Supported condition operators for rules.

    Used for comparing sensor values against thresholds.
    """

    GT = ">"
    LT = "<"
    GTE = ">="
    LTE = "<="
    EQ = "=="
    NE = "!="


class RuleExecutionStatus(PyEnum):
    """Status of rule execution attempt.

    Tracks the outcome of each rule evaluation cycle.
    """

    TRIGGERED = "triggered"
    SENT = "sent"
    COOLDOWN_SKIPPED = "cooldown_skipped"
    DAILY_LIMIT_REACHED = "daily_limit_reached"
    DURATION_NOT_MET = "duration_not_met"
    HYSTERESIS_ACTIVE = "hysteresis_active"
    ERROR = "error"


class Rule(Base):
    """Automation rule definition.

    A rule monitors a source device's metric and triggers an action
    on a target device when the condition is met.

    Key features:
    - Multi-tenant support via tenant UUID
    - Condition evaluation with operators (>, <, >=, <=, ==, !=)
    - Duration-based triggering (condition must be true for N seconds)
    - Hysteresis to prevent oscillation
    - Cooldown period between executions
    - Daily execution limits
    - Priority-based conflict resolution

    Example:
        Rule: "Turn off heating when temperature > 25C"
        - source_device_id: "sensor_temp_room1"
        - source_metric: "numeric_value"
        - condition_operator: ">"
        - condition_value: 25.0
        - target_device_id: "supla_relay_heating"
        - action: "turn_off"

    Attributes:
        id: Primary key (UUID)
        tenant: Tenant UUID for multi-tenancy

        # Source device (sensor)
        source_device_id: meter_id of the sensor device
        source_metric: Metric to monitor (e.g., 'numeric_value')

        # Condition
        condition_operator: Comparison operator ('>', '<', etc.)
        condition_value: Threshold value
        condition_duration_seconds: Required duration for condition to be true

        # Hysteresis
        hysteresis_value: Dead band to prevent oscillation

        # Target device (actuator)
        target_device_id: meter_id of the actuator device
        action: Action to execute ('turn_on', 'turn_off', etc.)
        action_params: JSON parameters for the action

        # Execution limits
        cooldown_seconds: Minimum time between executions
        max_executions_per_day: Optional daily limit
        priority: Higher values = higher priority

        # State
        is_active: Whether rule is enabled
        last_triggered_at: Timestamp of last successful trigger
        last_condition_true_at: When condition became true (for duration)
        executions_today: Counter for daily limit tracking

        # Audit
        created_at: Record creation timestamp
        updated_at: Last update timestamp
        created_by: Rule creator identifier
    """

    __tablename__ = "rules"

    # Primary key
    id = Column(
        UUID(as_uuid=True),
        primary_key=True,
        server_default=text("gen_random_uuid()"),
    )

    # Multi-tenant support
    tenant = Column(UUID(as_uuid=True), nullable=False, index=True)

    # Metadata
    name = Column(Text, nullable=False)
    description = Column(Text, nullable=True)

    # Source device (sensor)
    source_device_id = Column(Text, nullable=False, index=True)
    source_metric = Column(Text, nullable=False, server_default="numeric_value")

    # Condition
    condition_operator = Column(Text, nullable=False)
    condition_value = Column(Numeric(15, 4), nullable=False)
    condition_duration_seconds = Column(Integer, server_default="0", nullable=False)

    # Hysteresis
    hysteresis_value = Column(Numeric(15, 4), nullable=True)

    # Target device (actuator)
    target_device_id = Column(Text, nullable=False, index=True)
    action = Column(Text, nullable=False)
    action_params = Column(JSONB, server_default="{}", nullable=False)

    # Execution limits
    cooldown_seconds = Column(Integer, server_default="300", nullable=False)
    max_executions_per_day = Column(Integer, nullable=True)

    # Priority
    priority = Column(Integer, server_default="0", nullable=False)

    # State
    is_active = Column(Boolean, server_default="true", nullable=False)
    last_triggered_at = Column(DateTime(timezone=True), nullable=True)
    last_condition_true_at = Column(DateTime(timezone=True), nullable=True)
    executions_today = Column(Integer, server_default="0", nullable=False)

    # Audit
    created_at = Column(
        DateTime(timezone=True),
        nullable=False,
        server_default=text("NOW()"),
    )
    updated_at = Column(
        DateTime(timezone=True),
        nullable=False,
        server_default=text("NOW()"),
        onupdate=datetime.utcnow,
    )
    created_by = Column(Text, nullable=True)

    # Relationships
    executions = relationship(
        "RuleExecution",
        back_populates="rule",
        cascade="all, delete-orphan",
    )

    __table_args__ = (
        # Constraints
        CheckConstraint(
            "condition_operator IN ('>', '<', '>=', '<=', '==', '!=')",
            name="chk_rules_condition_operator",
        ),
        CheckConstraint(
            "cooldown_seconds >= 0",
            name="chk_rules_cooldown_positive",
        ),
        CheckConstraint(
            "condition_duration_seconds >= 0",
            name="chk_rules_duration_positive",
        ),
        # Indexes
        Index(
            "idx_rules_is_active",
            "is_active",
            postgresql_where="is_active = TRUE",
        ),
        Index(
            "idx_rules_tenant_active",
            "tenant",
            "is_active",
            postgresql_where="is_active = TRUE",
        ),
    )

    def __repr__(self):
        return (
            f"<Rule(id={self.id}, name={self.name}, "
            f"condition='{self.source_device_id} {self.condition_operator} "
            f"{self.condition_value}', action={self.action})>"
        )

    def evaluate_condition(self, current_value: float) -> bool:
        """Check if condition is met for given value.

        Args:
            current_value: Current sensor reading

        Returns:
            True if condition is met, False otherwise
        """
        ops = {
            ">": lambda a, b: a > b,
            "<": lambda a, b: a < b,
            ">=": lambda a, b: a >= b,
            "<=": lambda a, b: a <= b,
            "==": lambda a, b: a == b,
            "!=": lambda a, b: a != b,
        }
        return ops[self.condition_operator](current_value, float(self.condition_value))

    def is_in_cooldown(self, now: datetime) -> bool:
        """Check if rule is still in cooldown period.

        Args:
            now: Current timestamp (should be timezone-aware)

        Returns:
            True if cooldown is active, False if ready to trigger
        """
        if not self.last_triggered_at:
            return False

        # Handle timezone-naive timestamps
        last_triggered = self.last_triggered_at
        if last_triggered.tzinfo is None and now.tzinfo is not None:
            from datetime import timezone

            last_triggered = last_triggered.replace(tzinfo=timezone.utc)

        elapsed = (now - last_triggered).total_seconds()
        return elapsed < self.cooldown_seconds

    def check_hysteresis(self, current_value: float, was_triggered: bool) -> bool:
        """Check if hysteresis prevents re-triggering.

        Hysteresis creates a dead band around the threshold to prevent
        rapid on/off oscillations when value hovers near threshold.

        For "turn off when temp > 25" with hysteresis=2:
        - Triggers at 25.1C
        - Won't re-trigger until temp drops below 23C

        Args:
            current_value: Current sensor reading
            was_triggered: Whether rule was previously triggered

        Returns:
            True if hysteresis is blocking re-trigger
        """
        if not self.hysteresis_value or not was_triggered:
            return False

        hysteresis = float(self.hysteresis_value)
        threshold = float(self.condition_value)

        # For > or >= operators, hysteresis is subtracted
        if self.condition_operator in (">", ">="):
            hysteresis_threshold = threshold - hysteresis
            return current_value >= hysteresis_threshold

        # For < or <= operators, hysteresis is added
        if self.condition_operator in ("<", "<="):
            hysteresis_threshold = threshold + hysteresis
            return current_value <= hysteresis_threshold

        return False


class RuleExecution(Base):
    """Audit log entry for rule execution attempts.

    Records every evaluation of a rule, whether it resulted in a command
    being sent or was skipped due to cooldown, daily limit, etc.

    Attributes:
        id: Primary key (UUID)
        rule_id: Reference to the evaluated rule
        tenant: Tenant UUID

        # Execution context
        triggered_value: The sensor value at evaluation time
        threshold_value: The threshold at evaluation time

        # Result
        status: Outcome of the evaluation
        command_id: ID of created command (if sent to ESB)
        error_message: Error details (if status is 'error')

        # Timing
        evaluated_at: When evaluation occurred
        condition_true_since: When condition first became true (for duration)

        # Audit
        created_at: Record creation timestamp
    """

    __tablename__ = "rule_executions"

    # Primary key
    id = Column(
        UUID(as_uuid=True),
        primary_key=True,
        server_default=text("gen_random_uuid()"),
    )

    # Rule reference
    rule_id = Column(
        UUID(as_uuid=True),
        ForeignKey("rules.id", ondelete="CASCADE"),
        nullable=False,
        index=True,
    )
    tenant = Column(UUID(as_uuid=True), nullable=False, index=True)

    # Execution context
    triggered_value = Column(Numeric(15, 4), nullable=False)
    threshold_value = Column(Numeric(15, 4), nullable=False)

    # Result
    status = Column(Text, nullable=False)
    command_id = Column(UUID(as_uuid=True), nullable=True)
    error_message = Column(Text, nullable=True)

    # Timing
    evaluated_at = Column(
        DateTime(timezone=True),
        nullable=False,
        server_default=text("NOW()"),
    )
    condition_true_since = Column(DateTime(timezone=True), nullable=True)

    # Audit
    created_at = Column(
        DateTime(timezone=True),
        nullable=False,
        server_default=text("NOW()"),
    )

    # Relationships
    rule = relationship("Rule", back_populates="executions")

    __table_args__ = (
        Index("idx_rule_executions_created_at", "created_at"),
        Index("idx_rule_executions_status", "status"),
        Index(
            "idx_rule_executions_command_id",
            "command_id",
            postgresql_where="command_id IS NOT NULL",
        ),
    )

    def __repr__(self):
        return (
            f"<RuleExecution(id={self.id}, rule_id={self.rule_id}, "
            f"status={self.status}, value={self.triggered_value})>"
        )
