import asyncio
import logging
from datetime import datetime, timezone

import db
import mail

logger = logging.getLogger(__name__)

# Base polling interval — how often the loop wakes up to see which rules are due.
# Individual rules control their own check_interval (stored in alarm_rule.check_interval).
_BASE_INTERVAL = 60  # seconds

_OPERATORS = {
    "<": lambda v, t: v < t,
    ">": lambda v, t: v > t,
    "<=": lambda v, t: v <= t,
    ">=": lambda v, t: v >= t,
    "=": lambda v, t: v == t,
}


def _condition_met(operator: str, value: float, threshold: float) -> bool:
    fn = _OPERATORS.get(operator)
    return fn(value, threshold) if fn else False


async def _check_rules() -> None:
    # Only fetch rules whose check_interval has elapsed since last_checked_at
    rules = await db.fetch(
        """
        SELECT id, name, department_id, entity, metric, operator, threshold, check_interval
        FROM alarm_rule
        WHERE enabled = TRUE
          AND (
              last_checked_at IS NULL
              OR last_checked_at < NOW() - (check_interval || ' seconds')::INTERVAL
          )
        """
    )

    for rule in rules:
        rule_id = str(rule["id"])
        rule_name = rule["name"]
        department_id = str(rule["department_id"])
        entity = rule["entity"]
        metric = rule["metric"]
        operator = rule["operator"]
        threshold = rule["threshold"]

        # Stamp last_checked_at immediately so concurrent checker instances don't double-fire
        await db.execute(
            "UPDATE alarm_rule SET last_checked_at = NOW() WHERE id = $1::uuid",
            rule_id,
        )

        if operator == "no_data":
            if metric:
                latest = await db.fetchrow(
                    """
                    SELECT MAX(time) AS last_seen
                    FROM sensor_data
                    WHERE department_id = $1::uuid
                      AND entity        = $2
                      AND metric        = $3
                    """,
                    department_id,
                    entity,
                    metric,
                )
            else:
                latest = await db.fetchrow(
                    """
                    SELECT MAX(time) AS last_seen
                    FROM sensor_data
                    WHERE department_id = $1::uuid
                      AND entity        = $2
                    """,
                    department_id,
                    entity,
                )
            if latest is None or latest["last_seen"] is None:
                breach = True
                current_value = None
                occurred_at = None
            else:
                minutes_ago = (
                    datetime.now(timezone.utc) - latest["last_seen"]
                ).total_seconds() / 60
                breach = minutes_ago > threshold
                current_value = round(minutes_ago, 1)
                occurred_at = latest["last_seen"]
        else:
            # Most recent sensor reading for this rule's target
            latest = await db.fetchrow(
                """
                SELECT value, time
                FROM sensor_data
                WHERE department_id = $1::uuid
                  AND entity        = $2
                  AND metric        = $3
                ORDER BY time DESC
                LIMIT 1
                """,
                department_id,
                entity,
                metric,
            )

            if latest is None:
                continue

            current_value = latest["value"]
            occurred_at = latest["time"]
            breach = _condition_met(operator, current_value, threshold)

        await db.execute(
            "UPDATE alarm_rule SET last_checked_value = $1 WHERE id = $2::uuid",
            current_value,
            rule_id,
        )

        # Last alarm event for this rule (determines current state)
        last_event = await db.fetchrow(
            """
            SELECT status
            FROM alarm_event
            WHERE rule_id = $1::uuid
            ORDER BY occurred_at DESC
            LIMIT 1
            """,
            rule_id,
        )

        last_status = last_event["status"] if last_event else None

        if breach and last_status != "triggered":
            await db.execute(
                "INSERT INTO alarm_event (rule_id, status, value) VALUES ($1::uuid, 'triggered', $2)",
                rule_id,
                current_value,
            )

            recipients = await db.fetch(
                """
                SELECT ua.email, ua.name
                FROM alarm_recipient ar
                JOIN user_account ua ON ua.id = ar.user_id
                WHERE ar.rule_id = $1::uuid
                """,
                rule_id,
            )

            subject = f"LEAF Alarm: {rule_name}"
            if operator == "no_data":
                metric_line = f"Metric    : {metric}\n" if metric else ""
                body = (
                    f'Alarm "{rule_name}" has been triggered.\n\n'
                    f"Entity    : {entity}\n"
                    + metric_line
                    + f"Condition : no data for > {threshold:.0f} minutes\n"
                ) + (
                    f"Silent for: {current_value} minutes\n"
                    f"Last seen : {occurred_at.strftime('%Y-%m-%d %H:%M:%S UTC')}\n"
                    if occurred_at
                    else "Last seen : never\n"
                )
            else:
                body = (
                    f'Alarm "{rule_name}" has been triggered.\n\n'
                    f"Entity    : {entity}\n"
                    f"Metric    : {metric}\n"
                    f"Condition : {metric} {operator} {threshold}\n"
                    f"Value     : {current_value}\n"
                    f"Time      : {occurred_at.strftime('%Y-%m-%d %H:%M:%S UTC')}\n"
                )

            for recipient in recipients:
                try:
                    await mail.send_email(
                        to=recipient["email"], subject=subject, body=body
                    )
                except Exception:
                    logger.exception(
                        "Failed to send alarm email to %s for rule %s",
                        recipient["email"],
                        rule_id,
                    )

            logger.info(
                "Alarm triggered: rule=%s entity=%s metric=%s operator=%s",
                rule_id,
                entity,
                metric,
                operator,
            )

        elif not breach and last_status == "triggered":
            await db.execute(
                "INSERT INTO alarm_event (rule_id, status, value) VALUES ($1::uuid, 'resolved', $2)",
                rule_id,
                current_value,
            )
            logger.info(
                "Alarm resolved: rule=%s entity=%s metric=%s operator=%s",
                rule_id,
                entity,
                metric,
                operator,
            )


async def alarm_checker_loop() -> None:
    logger.info("Alarm checker waiting for schema...")
    while not await db.schema_applied():
        await asyncio.sleep(5)
    logger.info("Alarm checker started (base interval=%ds)", _BASE_INTERVAL)
    while True:
        try:
            await _check_rules()
        except Exception:
            logger.exception("Alarm checker error")
        await asyncio.sleep(_BASE_INTERVAL)
