from django.db import models
from django.utils import timezone


class CheckinLog(models.Model):
    """
    One row per check-in received from a tenant install.
    Kept so you can see history and spot a site that stopped reporting.
    """
    tenant       = models.ForeignKey(
        'tenants.Tenant', on_delete=models.CASCADE, related_name='checkins'
    )
    ft_version   = models.CharField(max_length=20, blank=True, default='')
    os_label     = models.CharField(max_length=100, blank=True, default='')
    db_engine    = models.CharField(max_length=30, blank=True, default='')
    plan         = models.CharField(max_length=20, blank=True, default='')
    module_count = models.PositiveSmallIntegerField(default=0)
    directives   = models.JSONField(default=dict, blank=True, help_text='What Final Target sent back')
    source_ip    = models.GenericIPAddressField(null=True, blank=True)
    created_at   = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']
        verbose_name = 'Check-in'
        verbose_name_plural = 'Check-ins'

    def __str__(self):
        return f'{self.tenant.name} v{self.ft_version} at {self.created_at:%Y-%m-%d %H:%M}'

class UptimeSample(models.Model):
    """
    One reachability check of one site. Written by `manage.py ping_sites`, which
    is meant to run on a schedule. Check-ins tell us a site was alive at some
    point in a day; this tells us minute by minute, which is what an uptime
    chart actually needs.
    """
    tenant      = models.ForeignKey(
        'tenants.Tenant', on_delete=models.CASCADE, related_name='uptime_samples'
    )
    checked_at  = models.DateTimeField(auto_now_add=True, db_index=True)
    ok          = models.BooleanField(default=False)
    status_code = models.PositiveSmallIntegerField(null=True, blank=True)
    latency_ms  = models.PositiveIntegerField(null=True, blank=True)
    error       = models.CharField(max_length=200, blank=True)

    class Meta:
        ordering = ['-checked_at']
        verbose_name = 'Uptime Sample'
        verbose_name_plural = 'Uptime Samples'
        indexes = [models.Index(fields=['tenant', 'checked_at'])]

    def __str__(self):
        state = 'up' if self.ok else 'down'
        return f'{self.tenant.name} {state} at {self.checked_at:%Y-%m-%d %H:%M}'

class Incident(models.Model):
    """
    A continuous stretch of a site being unreachable. Opened by `ping_sites` on
    the first failed check, closed on the first success after it, with a cause
    worked out at the time rather than guessed at later.
    """
    CAUSE_DNS        = 'dns'
    CAUSE_CONNECT    = 'connect'
    CAUSE_TIMEOUT    = 'timeout'
    CAUSE_TLS        = 'tls'
    CAUSE_TLS_EXPIRED = 'tls_expired'
    CAUSE_HTTP_5XX   = 'http_5xx'
    CAUSE_HTTP_4XX   = 'http_4xx'
    CAUSE_REDIRECT   = 'redirect'
    CAUSE_UNKNOWN    = 'unknown'

    CAUSE_CHOICES = [
        (CAUSE_DNS,         'DNS not resolving'),
        (CAUSE_CONNECT,     'Server refused the connection'),
        (CAUSE_TIMEOUT,     'Server did not respond in time'),
        (CAUSE_TLS,         'TLS handshake failed'),
        (CAUSE_TLS_EXPIRED, 'TLS certificate expired'),
        (CAUSE_HTTP_5XX,    'Application error'),
        (CAUSE_HTTP_4XX,    'Blocked or not found'),
        (CAUSE_REDIRECT,    'Redirect loop'),
        (CAUSE_UNKNOWN,     'Unclassified'),
    ]

    tenant     = models.ForeignKey('tenants.Tenant', on_delete=models.CASCADE, related_name='incidents')
    started_at = models.DateTimeField(db_index=True)
    ended_at   = models.DateTimeField(null=True, blank=True)

    cause        = models.CharField(max_length=20, choices=CAUSE_CHOICES, default=CAUSE_UNKNOWN)
    status_code  = models.PositiveSmallIntegerField(null=True, blank=True)
    error        = models.CharField(max_length=250, blank=True)
    diagnosis    = models.TextField(blank=True, help_text='What the probes found at the time')
    suggested_fix = models.CharField(max_length=250, blank=True)
    fail_count   = models.PositiveIntegerField(default=1)

    class Meta:
        ordering = ['-started_at']
        verbose_name = 'Incident'
        verbose_name_plural = 'Incidents'
        indexes = [models.Index(fields=['tenant', 'started_at'])]

    def __str__(self):
        return f'{self.tenant.name} {self.get_cause_display()} at {self.started_at:%Y-%m-%d %H:%M}'

    @property
    def is_open(self):
        return self.ended_at is None

    @property
    def duration(self):
        end = self.ended_at or timezone.now()
        return end - self.started_at


class JobRun(models.Model):
    """
    When each scheduled command last finished.

    Cron failing is silent by design. The job simply stops, and nothing on any
    page changes to say so. ping_sites is the worst case: Service Health keeps
    showing the last sample it managed to take and reports every site as up,
    so the page does not go blank, it lies.

    One row per command name, rewritten on each run, so this table never grows
    past the number of jobs.
    """
    name        = models.CharField(max_length=60, unique=True)
    last_run_at = models.DateTimeField(auto_now=True)
    ok          = models.BooleanField(default=True)
    note        = models.CharField(max_length=200, blank=True)

    class Meta:
        ordering = ['name']
        verbose_name = 'Job Run'
        verbose_name_plural = 'Job Runs'

    def __str__(self):
        return f'{self.name} at {self.last_run_at:%Y-%m-%d %H:%M}'

    @classmethod
    def record(cls, name, ok=True, note=''):
        """
        Called by a command as it finishes. Never raises: a monitoring row is
        not worth failing a real job over, and a job that dies trying to say
        it succeeded is worse than no heartbeat at all.
        """
        try:
            obj, _ = cls.objects.update_or_create(
                name=name, defaults={'ok': bool(ok), 'note': str(note)[:200]},
            )
            return obj
        except Exception:
            return None
