from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils import timezone
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from organisations.models import *
from django.db.models import JSONField
import hashlib
import hmac
import secrets
import re
from django.contrib.auth.hashers import check_password


def user_directory_path(instance, filename):
    # file will be uploaded to MEDIA_ROOT/username/<filename>
    return instance.name +'{0}/profile_pic'

def user_file_storage():
    fs = FileSystemStorage()
    return fs

class Staff(models.Model):
    GENDER = (('M', 'Male'),
        ('F', 'Female'),
        ('O', 'Other'),
        )
    date_added = models.DateTimeField(default = timezone.now)
    name = models.CharField(max_length = 255, null=False, blank=True)
    phone_number = models.CharField(max_length=15, null=True, blank=True)
    other_phone  = models.CharField(max_length=15, null=True, blank=True)
    email = models.EmailField(max_length = 50, null=True, blank=True)
    profile_url = models.TextField(blank=True, null=True)
    dob =  models.DateField( null=True, blank=True)
    nin = models.CharField(max_length = 20, null=True, blank=True)
    passport_number = models.CharField(max_length = 50, null=True, blank=True)
    nationality = models.CharField(max_length = 255, default="Ugandan")
    gender = models.CharField(max_length = 25, choices=GENDER,default="O")
    marital_status = models.CharField(max_length = 255, null=True, blank=True)
    occupation = models.CharField(max_length = 255, null=True, blank=True)
    staff_number = models.CharField(max_length = 50, null=True, blank=True)
    staff_added_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='st_added_by', null=True, blank=True)
    status     =  models.CharField(max_length=25, null=True, blank=True)
    is_active  = models.BooleanField(default=True)
    staff_organisation =  models.ForeignKey(Organisation, on_delete=models.CASCADE, related_name='st_org')

    def __str__(self):
        return self.name
    class Meta:
        db_table = 'public\".\"staff'

class User(AbstractUser):
    date_added = models.DateTimeField(default = timezone.now)
    updated_password = models.BooleanField(default=False)
    user_added_by = models.BigIntegerField(null=True, blank=True)
    status =  models.CharField(max_length=25, null=True, blank=True)
    user_organisation_branch =  models.ForeignKey(OrganisationBranch, on_delete=models.CASCADE, related_name='usr_branch_org')
    user_staff =  models.ForeignKey(Staff, on_delete=models.CASCADE, related_name='user_st')
    is_active  = models.BooleanField(default=True)
    assigned_days = models.IntegerField(null=False, blank=True, default=0)
    pass_reset_date = models.DateTimeField(default = timezone.now)
    two_fa_status  = models.BooleanField(default=False)
    pin            = models.CharField(max_length=128, null=True)  # Increased for hash
    pin_expires_at = models.DateTimeField(null=True, blank=True)
    otp_attempts   = models.IntegerField(default=0)
    otp_locked_until = models.DateTimeField(null=True, blank=True)
    last_otp_request = models.DateTimeField(null=True, blank=True)

    def _normalize_dt(self, dt_value):
        """Normalize legacy naive datetimes to timezone-aware values."""
        if not dt_value:
            return dt_value
        if timezone.is_naive(dt_value):
            return timezone.make_aware(dt_value, timezone.get_current_timezone())
        return dt_value
    
    def _build_otp_hash(self, otp, salt):
        """
        Build a compact OTP hash that fits legacy DB schemas where pin may be varchar(50).
        Format: v1$<salt>$<32-hex-digest> -> max length 45.
        """
        otp_value = self._normalize_otp_value(otp)
        digest = hashlib.sha256(f"{salt}:{otp_value}".encode("utf-8")).hexdigest()[:32]
        return f"v1${salt}${digest}"

    def _normalize_otp_value(self, otp):
        """Normalize OTP from client input (e.g. '4 8 3 5' -> '4835')."""
        return re.sub(r"\D", "", str(otp or ""))

    def set_otp(self, otp):
        """Hash and store OTP with expiration."""
        salt = secrets.token_hex(4)  # 8 chars
        self.pin = self._build_otp_hash(otp, salt)
        self.pin_expires_at = timezone.now() + timezone.timedelta(minutes=5)
        self.otp_attempts = 0
        self.otp_locked_until = None
        self.save(update_fields=["pin", "pin_expires_at", "otp_attempts", "otp_locked_until"])
    
    def verify_otp(self, otp):
        """Verify OTP and check expiration."""
        if not self.pin or not self.pin_expires_at:
            return False
        otp_value = self._normalize_otp_value(otp)
        if not otp_value:
            return False
        pin_expires_at = self._normalize_dt(self.pin_expires_at)
        if timezone.now() > pin_expires_at:
            return False

        stored_pin = str(self.pin)
        if stored_pin.startswith("v1$"):
            try:
                version, salt, expected = stored_pin.split("$", 2)
            except ValueError:
                return False
            if version != "v1" or not salt or not expected:
                return False
            actual = self._build_otp_hash(otp_value, salt).split("$", 2)[2]
            return hmac.compare_digest(actual, expected)

        # Backward compatibility for OTPs stored with make_password in older code.
        try:
            return check_password(otp_value, stored_pin)
        except Exception:
            return False
    
    def is_otp_locked(self):
        """Check if user is locked due to failed attempts"""
        otp_locked_until = self._normalize_dt(self.otp_locked_until)
        if otp_locked_until and timezone.now() < otp_locked_until:
            return True
        return False
    
    def can_request_otp(self):
        """Check rate limiting for OTP requests (1 per minute)"""
        last_otp_request = self._normalize_dt(self.last_otp_request)
        if last_otp_request:
            return timezone.now() > last_otp_request + timezone.timedelta(minutes=1)
        return True

    def increment_otp_attempts(self):
        """Increment failed OTP attempts and lock if needed"""
        self.otp_attempts += 1
        if self.otp_attempts >= 5:
            self.otp_locked_until = timezone.now() + timezone.timedelta(minutes=15)
        self.save(update_fields=["otp_attempts", "otp_locked_until"])
    
    def reset_otp_attempts(self):
        """Reset OTP attempts on successful verification"""
        self.otp_attempts = 0
        self.otp_locked_until = None
        self.pin = None
        self.pin_expires_at = None
        self.save(update_fields=["otp_attempts", "otp_locked_until", "pin", "pin_expires_at"])

    def __str__(self):
        return self.username

class AuditCategory(models.Model):
    title          =  models.CharField(max_length=100,null=False,blank=False)
    description    =  models.CharField(max_length=100,null=True,blank=True)
    action_key     =  models.CharField(max_length=100,null=True,blank=True)
    parent_id   =  models.BigIntegerField(null=True,blank=True)
    class Meta:
        db_table = 'public\".\"audit_category'
        
class UserAssignedRole(models.Model):
    date_added = models.DateTimeField(default = timezone.now)
    assigned_role = models.ForeignKey(UserRole, on_delete=models.CASCADE, related_name='assigned_role')
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='user_assigned')
    assigned_role_added_by =  models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='assigned_role_added_by')
    is_active  = models.BooleanField(default=True)
    class Meta:
        db_table = 'public\".\"user_assigned_role'

class UserSession(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='loggedinuser')
    session_token = models.TextField(null=True, blank=True)
    is_switched   = models.BooleanField(default=False)
    allow_access  = models.BooleanField(default=False)
    is_locked     = models.BooleanField(default=False)
    
    data = JSONField( null=False, blank=False)
    date_added  = models.DateTimeField(default = timezone.now)

    class Meta:
        db_table = 'public\".\"user_session_management'


class SwitchUserHistory(models.Model):
    new_user        = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='newloggedinuser')
    old_user        = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='oldloggedinuser')
    new_user_session_token = models.TextField(null=False, blank=False)
    old_user_session_token = models.TextField(null=False, blank=False)
    switch_in_date  = models.DateTimeField(default = timezone.now)
    switch_out_date = models.DateTimeField(null=True, blank=True)

    class Meta:
        db_table = 'public\".\"switch_user_history'


# view model for all components / permissions assigned to the user within an organisation.

class UserPermissions(models.Model):
    id            = models.IntegerField(primary_key=True)
    permission_id =  models.CharField(max_length=255)
    permission    =  models.CharField(max_length=255)
    desc        =  models.CharField(max_length=255)
    type        =  models.CharField(max_length=255)
    key         =  models.CharField(max_length=255)
    is_feature_active   = models.CharField(max_length=255)
    is_org_comp_active = models.CharField(max_length=255)
    org_id         = models.CharField(max_length=255)
    role_name      = models.CharField(max_length=255)
    role_desc      = models.CharField(max_length=255)
    is_role_active = models.CharField(max_length=255)
    is_role_component_active = models.CharField(max_length=255)
    date_added = models.CharField(max_length=255)
    user_id    = models.CharField(max_length=255)
    assigned_role_added_by_id = models.CharField(max_length=255)
    is_user_role_active       = models.CharField(max_length=255)

    class Meta:
        managed = False
        db_table = 'public\".\"user_permissions_view'
