import re

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.db.models import Q
from django.utils import timezone

RANGE_RE = re.compile(r"^(\d*)\s*-\s*(\d*)$")


def split_list(text):
    return [t.strip() for t in (text or "").split(",") if t.strip()]


def parse_districts(text):
    """'1-5, 12, Ilam, 70-'  ->  ids {12}, ranges [(1,5),(70,None)], names ['Ilam']"""
    ids, ranges, names = set(), [], []
    for tok in split_list(text):
        m = RANGE_RE.match(tok)
        if m and (m.group(1) or m.group(2)):
            lo = int(m.group(1)) if m.group(1) else None
            hi = int(m.group(2)) if m.group(2) else None
            if lo is not None and hi is not None and lo > hi:
                raise ValidationError(f"Bad range '{tok}': the start is bigger than the end.")
            ranges.append((lo, hi))
        elif tok.isdigit():
            ids.add(int(tok))
        else:
            names.append(tok)
    return ids, ranges, names


class SchoolQuerySet(models.QuerySet):
    def delete(self):
        count = self.filter(deleted_at__isnull=True).update(deleted_at=timezone.now())
        if count:
            from .cache import invalidate_finder_cache

            transaction.on_commit(invalidate_finder_cache)
        return count, {self.model._meta.label: count}

    def hard_delete(self):
        return super().delete()


class ActiveSchoolManager(models.Manager.from_queryset(SchoolQuerySet)):
    def get_queryset(self):
        return super().get_queryset().filter(deleted_at__isnull=True)


class AllSchoolManager(models.Manager.from_queryset(SchoolQuerySet)):
    pass


class School(models.Model):
    """One row per school. Key fields are real columns (fast filtering);
    the full original row, with every column, is kept in `data`."""
    organization_id = models.IntegerField(unique=True)
    name = models.CharField(max_length=300, db_index=True)
    district_id = models.IntegerField(null=True, blank=True, db_index=True)
    district_name = models.CharField(max_length=120, blank=True, db_index=True)
    municipality = models.CharField(max_length=200, blank=True, db_index=True)
    school_type = models.CharField(max_length=60, blank=True, db_index=True)
    total_students = models.IntegerField(null=True, blank=True, db_index=True)
    up_to_class = models.IntegerField(null=True, blank=True)
    data = models.JSONField(default=dict)
    deleted_at = models.DateTimeField(null=True, blank=True, db_index=True)

    objects = ActiveSchoolManager()
    all_objects = AllSchoolManager()

    class Meta:
        ordering = ["district_id", "organization_id"]
        default_manager_name = "objects"
        base_manager_name = "all_objects"

    def delete(self, using=None, keep_parents=False):
        if not self.pk:
            return 0, {}
        using = using or self._state.db
        deleted_at = timezone.now()
        count = School.all_objects.using(using).filter(
            pk=self.pk, deleted_at__isnull=True
        ).update(deleted_at=deleted_at)
        if count:
            self.deleted_at = deleted_at
            from .cache import invalidate_finder_cache

            transaction.on_commit(invalidate_finder_cache)
        return count, {self._meta.label: count}

    def restore(self, using=None):
        if not self.pk:
            return False
        using = using or self._state.db
        count = School.all_objects.using(using).filter(
            pk=self.pk, deleted_at__isnull=False
        ).update(deleted_at=None)
        if count:
            self.deleted_at = None
            from .cache import invalidate_finder_cache

            transaction.on_commit(invalidate_finder_cache)
        return bool(count)

    def hard_delete(self, using=None, keep_parents=False):
        return super().delete(using=using, keep_parents=keep_parents)

    def __str__(self):
        return f"{self.name} ({self.organization_id})"


class AccessProfile(models.Model):
    """Per-user settings: which columns they see and whether they may download."""
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
                                related_name="access_profile")
    visible_columns = models.JSONField(
        default=list, blank=True,
        help_text="Columns this user can see. Leave all unticked for the basic set "
                  "(code, district, municipality, ward, type, total students).")
    can_export = models.BooleanField(
        default=False, help_text="Show the Excel/CSV download buttons and allow /api/export.")

    CRM_CHOICES = [("none", "No CRM access"), ("view", "View only"), ("edit", "View and edit")]
    crm_access = models.CharField(
        max_length=4, choices=CRM_CHOICES, default="none",
        help_text="CRM = stages, priority, activity log, follow-ups. Only for schools this user may see.")
    crm_can_assign = models.BooleanField(
        default=False, verbose_name="Can assign leads to others",
        help_text="If off, the user can only claim leads for themselves.")

    def __str__(self):
        return f"Settings for {self.user}"


class AccessRule(models.Model):
    """One 'share' rule. A user sees every school that matches ANY of their active rules.
    Inside one rule, all filled-in boxes must match (AND)."""
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
                             related_name="access_rules")
    label = models.CharField(max_length=120, blank=True, help_text="Your own note, e.g. 'Eastern private small schools'")
    active = models.BooleanField(default=True)
    districts = models.CharField(
        max_length=500, blank=True,
        help_text="Comma-separated district ids, ranges or names. Examples: 1-5 | 12, 14 | 25- (25 onwards) | -10 | Ilam, Dhankuta. Blank = any district.")
    municipalities = models.CharField(max_length=800, blank=True,
                                      help_text="Comma-separated municipality names. Blank = any.")
    school_types = models.CharField(max_length=200, blank=True,
                                    help_text="Comma-separated, e.g. Private or Public, Private. Blank = any type.")
    min_students = models.PositiveIntegerField(null=True, blank=True,
                                               help_text="At least this many students in total.")
    max_students = models.PositiveIntegerField(null=True, blank=True,
                                               help_text="At most this many. For 'less than 50' enter 49.")
    min_up_to_class = models.PositiveIntegerField(null=True, blank=True, help_text="Teaches up to class ≥")
    max_up_to_class = models.PositiveIntegerField(null=True, blank=True, help_text="Teaches up to class ≤")

    def clean(self):
        parse_districts(self.districts)
        for lo, hi in ((self.min_students, self.max_students),
                       (self.min_up_to_class, self.max_up_to_class)):
            if lo is not None and hi is not None and lo > hi:
                raise ValidationError("A minimum is bigger than its maximum.")

    def to_q(self):
        """Django filter for this rule, or None if the rule has no conditions (= all schools)."""
        parts = []
        ids, ranges, names = parse_districts(self.districts)
        dq = Q()
        if ids:
            dq |= Q(district_id__in=ids)
        for lo, hi in ranges:
            r = Q()
            if lo is not None:
                r &= Q(district_id__gte=lo)
            if hi is not None:
                r &= Q(district_id__lte=hi)
            dq |= r
        for n in names:
            dq |= Q(district_name__iexact=n)
        if dq:
            parts.append(dq)
        mq = Q()
        for m in split_list(self.municipalities):
            mq |= Q(municipality__iexact=m)
        if mq:
            parts.append(mq)
        tq = Q()
        for t in split_list(self.school_types):
            tq |= Q(school_type__iexact=t)
        if tq:
            parts.append(tq)
        if self.min_students is not None:
            parts.append(Q(total_students__gte=self.min_students))
        if self.max_students is not None:
            parts.append(Q(total_students__lte=self.max_students))
        if self.min_up_to_class is not None:
            parts.append(Q(up_to_class__gte=self.min_up_to_class))
        if self.max_up_to_class is not None:
            parts.append(Q(up_to_class__lte=self.max_up_to_class))
        if not parts:
            return None
        q = parts[0]
        for p in parts[1:]:
            q &= p
        return q

    def describe(self):
        bits = []
        if self.districts: bits.append(f"districts {self.districts}")
        if self.municipalities: bits.append(f"municipalities {self.municipalities}")
        if self.school_types: bits.append(self.school_types)
        if self.min_students is not None: bits.append(f"students ≥ {self.min_students}")
        if self.max_students is not None: bits.append(f"students ≤ {self.max_students}")
        if self.min_up_to_class is not None: bits.append(f"up to class ≥ {self.min_up_to_class}")
        if self.max_up_to_class is not None: bits.append(f"up to class ≤ {self.max_up_to_class}")
        return " · ".join(bits) if bits else "ALL schools"

    def __str__(self):
        return (self.label + ": " if self.label else "") + self.describe()
