from __future__ import annotations

import ipaddress
import socket
import ssl
import time
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone as dt_timezone
from html import escape
from urllib.error import HTTPError, URLError
from urllib.parse import urlparse
from urllib.request import HTTPRedirectHandler, HTTPSHandler, Request, build_opener

from django.conf import settings
from django.db.models import Q
from django.utils import timezone

from .models import MonitorCheck, MonitorIncident, WebsiteMonitor


@dataclass
class ProbeResult:
    result: str
    http_status: int | None = None
    response_time_ms: int | None = None
    final_url: str = ""
    redirect_count: int = 0
    resolved_ip: str | None = None
    response_size_bytes: int | None = None
    content_match: bool | None = None
    ssl_valid: bool | None = None
    ssl_issuer: str = ""
    ssl_expires_at: datetime | None = None
    ssl_days_remaining: int | None = None
    error_type: str = ""
    error_message: str = ""
    details: dict = field(default_factory=dict)


class CountingRedirectHandler(HTTPRedirectHandler):
    def __init__(self, *, follow=True):
        super().__init__()
        self.follow = follow
        self.count = 0

    def redirect_request(self, req, fp, code, msg, headers, newurl):
        self.count += 1
        if not self.follow:
            return None
        return super().redirect_request(req, fp, code, msg, headers, newurl)


def parse_expected_status_codes(value: str) -> set[int]:
    value = (value or "").strip()
    if not value:
        raise ValueError("Indique pelo menos um código HTTP esperado.")
    result: set[int] = set()
    for item in value.split(","):
        item = item.strip()
        if not item:
            continue
        if "-" in item:
            start_raw, end_raw = item.split("-", 1)
            try:
                start, end = int(start_raw), int(end_raw)
            except ValueError as exc:
                raise ValueError(f"Intervalo HTTP inválido: {item}.") from exc
            if start > end or start < 100 or end > 599:
                raise ValueError(f"Intervalo HTTP inválido: {item}.")
            result.update(range(start, end + 1))
        else:
            try:
                code = int(item)
            except ValueError as exc:
                raise ValueError(f"Código HTTP inválido: {item}.") from exc
            if code < 100 or code > 599:
                raise ValueError(f"Código HTTP inválido: {item}.")
            result.add(code)
    if not result:
        raise ValueError("Indique pelo menos um código HTTP esperado.")
    return result


def _is_disallowed_ip(address: str) -> bool:
    ip = ipaddress.ip_address(address)
    return any(
        (
            ip.is_private,
            ip.is_loopback,
            ip.is_link_local,
            ip.is_multicast,
            ip.is_reserved,
            ip.is_unspecified,
        )
    )


def resolve_target(hostname: str, *, allow_private=False) -> str:
    try:
        addresses = socket.getaddrinfo(hostname, None, type=socket.SOCK_STREAM)
    except socket.gaierror as exc:
        raise RuntimeError(f"Não foi possível resolver o domínio: {exc}") from exc
    ips = []
    for entry in addresses:
        address = entry[4][0]
        if address not in ips:
            ips.append(address)
    if not ips:
        raise RuntimeError("O domínio não devolveu qualquer endereço IP.")
    global_allow = getattr(settings, "K4W_MONITOR_ALLOW_PRIVATE_NETWORKS", False)
    if not (allow_private or global_allow):
        blocked = [address for address in ips if _is_disallowed_ip(address)]
        if blocked:
            raise PermissionError(
                "O destino resolve para uma rede privada, local ou reservada. "
                "Ative explicitamente a monitorização de rede privada apenas quando for seguro."
            )
    return ips[0]


def _name_from_certificate(parts) -> str:
    values = []
    for group in parts or ():
        for key, value in group:
            if key in {"commonName", "organizationName"} and value:
                values.append(value)
    return " · ".join(dict.fromkeys(values))


def inspect_ssl(hostname: str, port: int, *, timeout: int) -> dict:
    context = ssl.create_default_context()
    with socket.create_connection((hostname, port), timeout=timeout) as raw_socket:
        with context.wrap_socket(raw_socket, server_hostname=hostname) as secure_socket:
            certificate = secure_socket.getpeercert()
            protocol = secure_socket.version() or ""
            cipher_data = secure_socket.cipher()
            cipher = cipher_data[0] if cipher_data else ""
    expires_raw = certificate.get("notAfter")
    issued_raw = certificate.get("notBefore")
    expires_at = None
    issued_at = None
    if expires_raw:
        expires_at = datetime.fromtimestamp(ssl.cert_time_to_seconds(expires_raw), tz=dt_timezone.utc)
    if issued_raw:
        issued_at = datetime.fromtimestamp(ssl.cert_time_to_seconds(issued_raw), tz=dt_timezone.utc)
    issuer = _name_from_certificate(certificate.get("issuer"))
    subject = _name_from_certificate(certificate.get("subject"))
    san = [value for kind, value in certificate.get("subjectAltName", ()) if kind == "DNS"]
    return {
        "valid": True,
        "issuer": issuer,
        "subject": subject,
        "issued_at": issued_at,
        "expires_at": expires_at,
        "covered_domains": san,
        "protocol": protocol,
        "cipher": cipher,
    }


def probe_website(monitor: WebsiteMonitor) -> ProbeResult:
    parsed = urlparse(monitor.url)
    if parsed.scheme not in {"http", "https"} or not parsed.hostname:
        return ProbeResult(
            result=MonitorCheck.Result.DOWN,
            error_type=MonitorCheck.ErrorType.SECURITY,
            error_message="URL HTTP/HTTPS inválida.",
        )

    try:
        resolved_ip = resolve_target(parsed.hostname, allow_private=monitor.allow_private_networks)
    except PermissionError as exc:
        return ProbeResult(
            result=MonitorCheck.Result.DOWN,
            error_type=MonitorCheck.ErrorType.SECURITY,
            error_message=str(exc),
        )
    except RuntimeError as exc:
        return ProbeResult(
            result=MonitorCheck.Result.DOWN,
            error_type=MonitorCheck.ErrorType.DNS,
            error_message=str(exc),
        )

    redirect_handler = CountingRedirectHandler(follow=monitor.follow_redirects)
    if parsed.scheme == "https":
        ssl_context = ssl.create_default_context() if monitor.verify_ssl else ssl._create_unverified_context()
        opener = build_opener(redirect_handler, HTTPSHandler(context=ssl_context))
    else:
        opener = build_opener(redirect_handler)

    request = Request(
        monitor.url,
        method=monitor.http_method,
        headers={
            "User-Agent": getattr(settings, "K4W_MONITOR_USER_AGENT", "Kreate4Web-Monitor/9.2"),
            "Accept": "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8",
            "Cache-Control": "no-cache",
        },
    )
    max_bytes = int(getattr(settings, "K4W_MONITOR_MAX_BODY_BYTES", 1_048_576))
    started = time.perf_counter()
    response = None
    body = b""
    try:
        try:
            response = opener.open(request, timeout=monitor.timeout_seconds)
        except HTTPError as exc:
            response = exc
        http_status = getattr(response, "status", None) or getattr(response, "code", None)
        final_url = response.geturl() if response else monitor.url
        if monitor.http_method == WebsiteMonitor.HttpMethod.GET or monitor.expected_text:
            body = response.read(max_bytes + 1) if response else b""
            if len(body) > max_bytes:
                body = body[:max_bytes]
        response_ms = max(1, int((time.perf_counter() - started) * 1000))
    except socket.timeout as exc:
        return ProbeResult(
            result=MonitorCheck.Result.DOWN,
            response_time_ms=max(1, int((time.perf_counter() - started) * 1000)),
            resolved_ip=resolved_ip,
            error_type=MonitorCheck.ErrorType.TIMEOUT,
            error_message=f"O website não respondeu dentro de {monitor.timeout_seconds} segundos.",
            details={"exception": str(exc)},
        )
    except ssl.SSLError as exc:
        return ProbeResult(
            result=MonitorCheck.Result.DOWN,
            response_time_ms=max(1, int((time.perf_counter() - started) * 1000)),
            resolved_ip=resolved_ip,
            ssl_valid=False,
            error_type=MonitorCheck.ErrorType.SSL,
            error_message=f"Falha SSL: {exc}",
        )
    except (URLError, ConnectionError, OSError) as exc:
        reason = getattr(exc, "reason", exc)
        error_type = MonitorCheck.ErrorType.TIMEOUT if isinstance(reason, socket.timeout) else MonitorCheck.ErrorType.CONNECTION
        return ProbeResult(
            result=MonitorCheck.Result.DOWN,
            response_time_ms=max(1, int((time.perf_counter() - started) * 1000)),
            resolved_ip=resolved_ip,
            error_type=error_type,
            error_message=f"Falha de ligação: {reason}",
        )
    finally:
        if response is not None:
            try:
                response.close()
            except Exception:
                pass

    expected = parse_expected_status_codes(monitor.expected_status_codes)
    content_match = None
    error_type = ""
    error_message = ""
    result = MonitorCheck.Result.UP

    if http_status not in expected:
        result = MonitorCheck.Result.DOWN
        error_type = MonitorCheck.ErrorType.HTTP
        error_message = f"O website devolveu HTTP {http_status}; esperado: {monitor.expected_status_codes}."

    if monitor.expected_text:
        decoded = body.decode("utf-8", errors="ignore")
        content_match = monitor.expected_text.lower() in decoded.lower()
        if not content_match:
            result = MonitorCheck.Result.DOWN
            error_type = MonitorCheck.ErrorType.CONTENT
            error_message = "O texto obrigatório não foi encontrado na resposta."

    ssl_valid = None
    ssl_issuer = ""
    ssl_expires_at = None
    ssl_days_remaining = None
    ssl_details = {}
    if parsed.scheme == "https" and monitor.inspect_ssl_certificate:
        try:
            ssl_details = inspect_ssl(parsed.hostname, parsed.port or 443, timeout=monitor.timeout_seconds)
            ssl_valid = True
            ssl_issuer = ssl_details.get("issuer", "")
            ssl_expires_at = ssl_details.get("expires_at")
            if ssl_expires_at:
                ssl_days_remaining = (ssl_expires_at.date() - timezone.localdate()).days
                if ssl_days_remaining < 0:
                    result = MonitorCheck.Result.DOWN
                    error_type = MonitorCheck.ErrorType.SSL
                    error_message = "O certificado SSL está expirado."
                elif result == MonitorCheck.Result.UP and ssl_days_remaining <= monitor.ssl_warning_days:
                    result = MonitorCheck.Result.DEGRADED
                    error_message = f"O certificado SSL expira dentro de {ssl_days_remaining} dias."
        except Exception as exc:
            ssl_valid = False
            if monitor.verify_ssl:
                result = MonitorCheck.Result.DOWN
                error_type = MonitorCheck.ErrorType.SSL
                error_message = f"Não foi possível validar o certificado SSL: {exc}"
            else:
                if result == MonitorCheck.Result.UP:
                    result = MonitorCheck.Result.DEGRADED
                error_message = f"Certificado SSL não validado: {exc}"

    if result == MonitorCheck.Result.UP and response_ms > monitor.warning_response_ms:
        result = MonitorCheck.Result.DEGRADED
        error_message = f"Resposta lenta: {response_ms} ms."

    headers = {}
    if response is not None:
        try:
            headers = {
                key: value
                for key, value in response.headers.items()
                if key.lower() in {"server", "content-type", "content-length", "location", "x-powered-by"}
            }
        except Exception:
            headers = {}

    return ProbeResult(
        result=result,
        http_status=http_status,
        response_time_ms=response_ms,
        final_url=final_url,
        redirect_count=redirect_handler.count,
        resolved_ip=resolved_ip,
        response_size_bytes=len(body) if body else 0,
        content_match=content_match,
        ssl_valid=ssl_valid,
        ssl_issuer=ssl_issuer,
        ssl_expires_at=ssl_expires_at,
        ssl_days_remaining=ssl_days_remaining,
        error_type=error_type,
        error_message=error_message,
        details={"headers": headers, "ssl": ssl_details},
    )


def _notification_body(monitor: WebsiteMonitor, incident: MonitorIncident, *, recovered=False) -> tuple[str, str, str]:
    client = monitor.technical_asset.service.client
    if recovered:
        subject = f"Website novamente operacional — {monitor.name}"
        body = (
            f"Olá {client.name},\n\n"
            f"A monitorização confirmou que {monitor.name} está novamente operacional.\n"
            f"URL: {monitor.url}\n"
            f"Incidente: {incident.opened_at:%d/%m/%Y %H:%M}\n\n"
            "Kreate4Web"
        )
    else:
        subject = f"Alerta de indisponibilidade — {monitor.name}"
        body = (
            f"Olá {client.name},\n\n"
            f"Foi detetado um problema em {monitor.name}.\n"
            f"URL: {monitor.url}\n"
            f"Motivo: {incident.reason}\n\n"
            "A equipa Kreate4Web foi alertada para analisar a situação."
        )
    html_body = (
        "<div style='font-family:Arial,sans-serif;max-width:640px;margin:auto;color:#20304a'>"
        "<h2 style='border-left:6px solid #ffd400;padding-left:12px'>Kreate4Web</h2>"
        f"<p>Olá <strong>{escape(client.name)}</strong>,</p>"
        f"<p>{escape(body.split(chr(10) + chr(10), 1)[1]).replace(chr(10), '<br>')}</p>"
        "</div>"
    )
    return subject, body, html_body


def _queue_incident_notification(monitor: WebsiteMonitor, incident: MonitorIncident, *, recovered=False):
    from core.models import PortalConfiguration
    from notifications.models import NotificationLog, NotificationTemplate

    config = PortalConfiguration.objects.order_by("pk").first()
    if not config:
        return []
    client = monitor.technical_asset.service.client
    event = "recovered" if recovered else "down"
    template_type = (
        NotificationTemplate.Type.MONITOR_RECOVERED
        if recovered
        else NotificationTemplate.Type.MONITOR_DOWN
    )
    template = NotificationTemplate.objects.filter(notification_type=template_type, is_active=True).order_by("pk").first()
    subject, body, html_body = _notification_body(monitor, incident, recovered=recovered)
    if template:
        variables = {
            "cliente": client.name,
            "servico": monitor.technical_asset.service.name,
            "dominio": monitor.technical_asset.domain_name or monitor.url,
            "website": monitor.name,
            "url": monitor.url,
            "motivo": incident.reason,
            "empresa": config.company_name,
            "email_empresa": config.company_email,
            "telefone_empresa": config.company_phone,
        }
        class SafeDict(dict):
            def __missing__(self, key):
                return "{" + key + "}"
        values = SafeDict(variables)
        subject = template.subject_template.format_map(values)
        body = template.body_template.format_map(values)
        html_body = template.html_body_template.format_map(values) if template.html_body_template else html_body

    recipients = []
    if monitor.notify_internal:
        internal = config.internal_notification_email or config.company_email
        if internal:
            recipients.append((internal, False))
    if monitor.notify_client and client.email:
        recipients.append((client.email, True))

    created = []
    for recipient, is_client in dict.fromkeys(recipients):
        key = f"monitor:{incident.pk}:{event}:{recipient.lower()}"
        notification, was_created = NotificationLog.objects.get_or_create(
            deduplication_key=key,
            defaults={
                "client": client,
                "service": monitor.technical_asset.service,
                "template": template,
                "channel": NotificationLog.Channel.EMAIL,
                "recipient": recipient,
                "original_recipient": recipient,
                "subject": subject,
                "body": body,
                "html_body": html_body,
                "status": NotificationLog.Status.SCHEDULED if config.auto_send_notifications else NotificationLog.Status.DRAFT,
                "scheduled_for": timezone.now(),
                "is_automatic": True,
                "max_attempts": config.notification_max_attempts,
            },
        )
        if was_created:
            created.append(notification)
    return created


def _create_incident_task(monitor: WebsiteMonitor, incident: MonitorIncident):
    if not monitor.create_task or incident.internal_task_id:
        return incident.internal_task
    from operations.models import InternalTask

    service = monitor.technical_asset.service
    task = InternalTask.objects.create(
        title=f"Investigar website indisponível: {monitor.name}",
        task_type=InternalTask.TaskType.TECHNICAL_CHECK,
        client=service.client,
        service=service,
        priority=InternalTask.Priority.URGENT,
        status=InternalTask.Status.TODO,
        due_date=timezone.localdate(),
        description=(
            f"Incidente automático de monitorização.\n"
            f"URL: {monitor.url}\n"
            f"Motivo: {incident.reason}\n"
            f"Falhas consecutivas: {monitor.consecutive_failures}."
        ),
    )
    incident.internal_task = task
    incident.save(update_fields=["internal_task", "updated_at"])
    return task


def _open_or_update_incident(monitor: WebsiteMonitor, check: MonitorCheck) -> MonitorIncident | None:
    if not monitor.create_incident:
        return None
    incident = monitor.incidents.filter(
        status__in=[MonitorIncident.Status.OPEN, MonitorIncident.Status.ACKNOWLEDGED]
    ).order_by("-opened_at").first()
    if incident:
        incident.last_check = check
        incident.failure_count += 1
        incident.reason = check.error_message or "Website indisponível."
        incident.save(update_fields=["last_check", "failure_count", "reason", "updated_at"])
        return incident
    incident = MonitorIncident.objects.create(
        monitor=monitor,
        title=f"Indisponibilidade detetada — {monitor.name}",
        reason=check.error_message or "Website indisponível.",
        severity=MonitorIncident.Severity.CRITICAL,
        status=MonitorIncident.Status.OPEN,
        first_check=check,
        last_check=check,
        failure_count=monitor.consecutive_failures,
        client_visible=monitor.client_visible,
    )
    _create_incident_task(monitor, incident)
    _queue_incident_notification(monitor, incident, recovered=False)
    return incident


def _resolve_open_incident(monitor: WebsiteMonitor, check: MonitorCheck) -> MonitorIncident | None:
    incident = monitor.incidents.filter(
        status__in=[MonitorIncident.Status.OPEN, MonitorIncident.Status.ACKNOWLEDGED]
    ).order_by("-opened_at").first()
    if not incident:
        return None
    incident.last_check = check
    incident.resolve(note="Recuperação confirmada automaticamente pela monitorização.")
    if incident.internal_task_id:
        from operations.models import InternalTask

        task = incident.internal_task
        if task.status not in [InternalTask.Status.DONE, InternalTask.Status.CANCELLED]:
            task.status = InternalTask.Status.DONE
            task.description = (task.description + "\n\nRecuperação confirmada automaticamente.").strip()
            task.save(update_fields=["status", "description", "completed_at", "updated_at"])
    _queue_incident_notification(monitor, incident, recovered=True)
    return incident


def _sync_related_assets(monitor: WebsiteMonitor, check: MonitorCheck):
    from technical.models import SSLCertificate, TechnicalAsset

    asset = monitor.technical_asset
    asset.last_checked_at = check.checked_at
    if check.result == MonitorCheck.Result.DOWN:
        asset.status = TechnicalAsset.Status.CRITICAL
    elif check.result == MonitorCheck.Result.DEGRADED:
        asset.status = TechnicalAsset.Status.ATTENTION
    elif check.result == MonitorCheck.Result.UP:
        asset.status = TechnicalAsset.Status.ACTIVE
    if check.ssl_valid is not None:
        asset.ssl_active = check.ssl_valid
    asset.save(update_fields=["last_checked_at", "status", "ssl_active", "updated_at"])

    if check.ssl_expires_at or check.ssl_valid is not None:
        certificate = SSLCertificate.objects.filter(technical_asset=asset).first()
        if certificate:
            certificate.last_checked_at = check.checked_at
            certificate.expires_at = check.ssl_expires_at or certificate.expires_at
            certificate.issuer = check.ssl_issuer or certificate.issuer
            certificate.error_message = check.error_message if check.error_type == MonitorCheck.ErrorType.SSL else ""
            if check.ssl_valid is False:
                certificate.status = SSLCertificate.Status.INVALID
            elif check.ssl_days_remaining is not None and check.ssl_days_remaining < 0:
                certificate.status = SSLCertificate.Status.EXPIRED
            elif check.ssl_days_remaining is not None and check.ssl_days_remaining <= monitor.ssl_warning_days:
                certificate.status = SSLCertificate.Status.EXPIRING
            elif check.ssl_valid:
                certificate.status = SSLCertificate.Status.VALID
            certificate.save(update_fields=[
                "last_checked_at", "expires_at", "issuer", "error_message", "status", "updated_at"
            ])


def run_monitor_check(monitor: WebsiteMonitor | int, *, probe=None, triggered_by=None) -> MonitorCheck:
    if isinstance(monitor, int):
        monitor = WebsiteMonitor.objects.select_related(
            "technical_asset__service__client"
        ).get(pk=monitor)
    else:
        monitor = WebsiteMonitor.objects.select_related(
            "technical_asset__service__client"
        ).get(pk=monitor.pk)

    now = timezone.now()
    if not monitor.is_active:
        monitor.status = WebsiteMonitor.Status.PAUSED
        monitor.next_check_at = None
        monitor.save(update_fields=["status", "next_check_at", "updated_at"])
        return MonitorCheck.objects.create(
            monitor=monitor,
            checked_at=now,
            result=MonitorCheck.Result.SKIPPED,
            error_message="Monitorização pausada.",
        )

    if monitor.maintenance_until and monitor.maintenance_until > now:
        monitor.status = WebsiteMonitor.Status.MAINTENANCE
        monitor.next_check_at = monitor.maintenance_until
        monitor.save(update_fields=["status", "next_check_at", "updated_at"])
        return MonitorCheck.objects.create(
            monitor=monitor,
            checked_at=now,
            result=MonitorCheck.Result.MAINTENANCE,
            error_message=f"Janela de manutenção até {timezone.localtime(monitor.maintenance_until):%d/%m/%Y %H:%M}.",
        )

    probe_result = (probe or probe_website)(monitor)
    check = MonitorCheck.objects.create(
        monitor=monitor,
        checked_at=now,
        result=probe_result.result,
        http_status=probe_result.http_status,
        response_time_ms=probe_result.response_time_ms,
        final_url=probe_result.final_url,
        redirect_count=probe_result.redirect_count,
        resolved_ip=probe_result.resolved_ip,
        response_size_bytes=probe_result.response_size_bytes,
        content_match=probe_result.content_match,
        ssl_valid=probe_result.ssl_valid,
        ssl_issuer=probe_result.ssl_issuer,
        ssl_expires_at=probe_result.ssl_expires_at,
        ssl_days_remaining=probe_result.ssl_days_remaining,
        error_type=probe_result.error_type,
        error_message=probe_result.error_message,
        details=probe_result.details,
    )

    monitor.last_checked_at = now
    monitor.next_check_at = now + timedelta(minutes=monitor.interval_minutes)
    monitor.last_response_ms = probe_result.response_time_ms
    monitor.last_http_status = probe_result.http_status
    monitor.last_error = probe_result.error_message

    if check.result == MonitorCheck.Result.DOWN:
        monitor.consecutive_failures += 1
        monitor.consecutive_successes = 0
        monitor.last_failure_at = now
        monitor.status = (
            WebsiteMonitor.Status.DOWN
            if monitor.consecutive_failures >= monitor.alert_after_failures
            else WebsiteMonitor.Status.DEGRADED
        )
    else:
        monitor.consecutive_failures = 0
        monitor.consecutive_successes += 1
        monitor.last_success_at = now
        monitor.status = (
            WebsiteMonitor.Status.DEGRADED
            if check.result == MonitorCheck.Result.DEGRADED
            else WebsiteMonitor.Status.UP
        )

    monitor.save(update_fields=[
        "last_checked_at", "next_check_at", "last_response_ms", "last_http_status", "last_error",
        "consecutive_failures", "consecutive_successes", "last_failure_at", "last_success_at", "status", "updated_at"
    ])
    _sync_related_assets(monitor, check)

    if check.result == MonitorCheck.Result.DOWN and monitor.consecutive_failures >= monitor.alert_after_failures:
        _open_or_update_incident(monitor, check)
    elif check.result in [MonitorCheck.Result.UP, MonitorCheck.Result.DEGRADED] and monitor.consecutive_successes >= monitor.recover_after_successes:
        _resolve_open_incident(monitor, check)
    return check


def run_due_monitors(*, limit=None) -> dict:
    now = timezone.now()
    queryset = WebsiteMonitor.objects.filter(is_active=True).filter(
        Q(next_check_at__isnull=True) | Q(next_check_at__lte=now)
    ).order_by("next_check_at", "pk")
    if limit:
        queryset = queryset[:limit]
    result = {"processed": 0, "up": 0, "degraded": 0, "down": 0, "errors": []}
    for monitor_id in queryset.values_list("pk", flat=True):
        try:
            check = run_monitor_check(monitor_id)
            result["processed"] += 1
            if check.result == MonitorCheck.Result.UP:
                result["up"] += 1
            elif check.result == MonitorCheck.Result.DEGRADED:
                result["degraded"] += 1
            elif check.result == MonitorCheck.Result.DOWN:
                result["down"] += 1
        except Exception as exc:
            result["errors"].append({"monitor_id": monitor_id, "error": str(exc)})
    return result


def cleanup_monitor_history(*, days=None) -> dict:
    days = int(days or getattr(settings, "K4W_MONITOR_HISTORY_DAYS", 180))
    threshold = timezone.now() - timedelta(days=days)
    protected_ids = set(
        MonitorIncident.objects.exclude(first_check__isnull=True).values_list("first_check_id", flat=True)
    ) | set(
        MonitorIncident.objects.exclude(last_check__isnull=True).values_list("last_check_id", flat=True)
    )
    queryset = MonitorCheck.objects.filter(checked_at__lt=threshold)
    if protected_ids:
        queryset = queryset.exclude(pk__in=protected_ids)
    deleted, breakdown = queryset.delete()
    return {"days": days, "deleted": deleted, "breakdown": breakdown}
