import secrets
import string
import time
import uuid

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.contrib.auth.models import AbstractUser, UserManager
from django.utils import timezone
from apps.core.uploads import tenant_upload_path, user_upload_path


def uuid7() -> uuid.UUID:
    """Generate a UUID v7 (time-ordered UUID)."""
    nanoseconds = time.time_ns()
    uuid_int = (nanoseconds << 16) | secrets.randbits(48)
    return uuid.UUID(int=uuid_int)


def generate_username() -> str:
    """Generate random 7-character alphanumeric username."""
    chars = string.ascii_uppercase + string.digits
    return ''.join(secrets.choice(chars) for _ in range(7))


class UserManager(UserManager):
    """Custom manager for email-based authentication."""

    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError("Email is required")
        email = self.normalize_email(email)
        extra_fields.setdefault("username", generate_username())
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", True)
        user = self.create_user(email, password, **extra_fields)

        from apps.companies.models import Organization, OrganizationMembership

        domain = email.split("@", 1)[1] if "@" in email else "new-organization"
        org = Organization.objects.create(name=domain)
        OrganizationMembership.objects.create(organization=org, user=user, is_owner=True)

        # The Employee/Contact auto-provisioning signals ran during
        # create_user() above, before this org/membership existed, so both
        # steps skipped themselves. Employee.objects.create() below re-fires
        # post_save, which chains into Contact creation too.
        from apps.hr.signals import ensure_employee_stub
        ensure_employee_stub(user)

        return user


class User(AbstractUser):
    """Extended user model with avatar and metadata."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    email = models.EmailField(unique=True)
    avatar = models.ImageField(upload_to=user_upload_path("avatar"), blank=True, null=True)
    phone = models.CharField(max_length=20, blank=True)
    bio = models.TextField(blank=True)
    must_change_password = models.BooleanField(default=False)
    onboarding_completed = models.BooleanField(default=True)
    organization_role = models.CharField(max_length=20, choices=[("staff", "Staff"), ("director", "Director"), ("donor", "Donor")], default="staff", db_index=True)
    account_status = models.CharField(max_length=24, choices=[("pending_activation", "Pending activation"), ("active", "Active"), ("suspended", "Suspended"), ("locked", "Locked")], default="active", db_index=True)
    donor_company = models.ForeignKey(
        "companies.Company", on_delete=models.SET_NULL, null=True, blank=True, related_name="donor_users",
        help_text="Set only for organization_role='donor' — the donor company this account represents in the donor portal.",
    )
    donor_role = models.CharField(
        max_length=10, choices=[("admin", "Donor Admin"), ("viewer", "Donor Viewer")], default="admin",
        help_text="Meaningful only for organization_role='donor': admins review/approve submissions and manage their org's portal users; viewers are read-only.",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []
    objects = UserManager()

    def save(self, *args, **kwargs):
        if not self.username:
            while True:
                username = generate_username()
                if not User.objects.filter(username=username).exists():
                    self.username = username
                    break
        super().save(*args, **kwargs)

    class Meta:
        db_table = "users"

    RBAC_CACHE_TTL = 60  # seconds; role changes also invalidate explicitly

    @staticmethod
    def rbac_cache_key(user_id, organization_id) -> str:
        from .authorization.cache import cache_key_part

        return f"rbac:perms:{cache_key_part(user_id)}:{cache_key_part(organization_id)}"

    def _role_permission_codenames(self, organization) -> set[str]:
        """Flattened set of `<domain>.<action>` codenames from roles assigned
        to this user WITHIN `organization`.

        Superuser short-circuits to {"*"}. Memoized per (instance, org) for
        the lifetime of this Python object (one request) and in the Django
        cache across requests (60s TTL, invalidated on role changes — see
        rbac_views). With LocMemCache other processes may stay stale up to
        the TTL.
        """
        if self.is_superuser:
            return {"*"}
        if not hasattr(self, "_rbac_codenames_cache"):
            self._rbac_codenames_cache = {}
        if organization.id in self._rbac_codenames_cache:
            return self._rbac_codenames_cache[organization.id]

        from django.core.cache import cache

        key = self.rbac_cache_key(self.pk, organization.id)
        cached = cache.get(key)
        if cached is None:
            pairs = Permission.objects.filter(
                roles__user_assignments__user=self,
                roles__user_assignments__organization=organization,
            ).filter(
                # v1 codenames come from legacy rows only; v2 capability rows
                # are resolved by apps.core.authorization.resolver.
                models.Q(code__isnull=True) | models.Q(code__startswith="legacy.")
            ).values_list("domain", "action").distinct()
            cached = {f"{d}.{a}" for d, a in pairs}
            cache.set(key, cached, self.RBAC_CACHE_TTL)
        self._rbac_codenames_cache[organization.id] = cached
        return cached

    def has_menu_perm(self, codename: str, organization) -> bool:
        perms = self._role_permission_codenames(organization)
        if "*" in perms or codename in perms:
            return True
        domain = codename.split(".", 1)[0]
        return f"{domain}.*" in perms


class Permission(models.Model):
    """Menu-domain permission (legacy v1) or capability (v2).

    v1 rows use `domain`/`action` (codename `<domain>.<action>`). v2 capability
    rows carry a `code` following the `<module>.<resource>.<verb>` grammar from
    the capability registry (apps.core.authorization.registry). During the
    expand phase both shapes coexist; `codename` prefers `code`.
    """
    RISK_LEVELS = [
        ("low", "Low"),
        ("medium", "Medium"),
        ("high", "High"),
        ("critical", "Critical"),
    ]

    domain = models.CharField(max_length=40)
    action = models.CharField(max_length=20)
    label = models.CharField(max_length=120)
    # v2 capability fields (expand phase; legacy rows get `legacy.<domain>.<action>`)
    code = models.CharField(max_length=120, unique=True, null=True, blank=True)
    module = models.CharField(max_length=40, db_index=True, blank=True)
    resource = models.CharField(max_length=60, blank=True)
    description = models.TextField(blank=True)
    risk_level = models.CharField(max_length=10, choices=RISK_LEVELS, default="low")

    class Meta:
        db_table = "rbac_permissions"
        ordering = ["domain", "action"]
        constraints = [
            # Legacy `<domain>.<action>` rows stay unique; v2 capability rows
            # (code set) may share (domain, action) pairs — `code` is unique.
            models.UniqueConstraint(
                fields=["domain", "action"],
                condition=models.Q(code__isnull=True),
                name="unique_legacy_permission_codename",
            ),
        ]

    def __str__(self):
        return self.codename

    @property
    def codename(self) -> str:
        return self.code or f"{self.domain}.{self.action}"


class Role(models.Model):
    """Named bundle of permissions, assignable to users."""
    name = models.CharField(max_length=80, unique=True)
    slug = models.SlugField(max_length=80, unique=True)
    description = models.TextField(blank=True)
    is_system = models.BooleanField(default=False)
    manager_role = models.ForeignKey(
        "self",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="managed_roles",
        help_text="Optional role responsible for approvals and reporting for holders of this role.",
    )
    permissions = models.ManyToManyField(Permission, related_name="roles", blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "rbac_roles"
        ordering = ["name"]

    def __str__(self):
        return self.name


class UserRole(models.Model):
    """User <-> Role assignment, scoped to one organization, with audit fields."""
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="user_roles")
    role = models.ForeignKey(Role, on_delete=models.CASCADE, related_name="user_assignments")
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="user_roles")
    assigned_at = models.DateTimeField(auto_now_add=True)
    assigned_by = models.ForeignKey(
        User, on_delete=models.SET_NULL, null=True, blank=True, related_name="+"
    )

    class Meta:
        db_table = "rbac_user_roles"
        ordering = ["-assigned_at"]
        constraints = [models.UniqueConstraint(fields=["user", "role", "organization"], name="unique_user_role_per_org")]

    def __str__(self):
        return f"{self.user.email} -> {self.role.name} ({self.organization.name})"


def _invitation_token() -> str:
    return secrets.token_urlsafe(32)


class Invitation(models.Model):
    """A pending grant of org membership (+ optional role) to an email address.

    Accepting creates the User (if the email is new), an OrganizationMembership,
    and a UserRole if `role` is set. The token is the sole proof of email
    ownership — it isn't tied to an existing User the way password-reset
    tokens are, since the invitee may not have an account yet.
    """
    STATUS_CHOICES = [
        ("pending", "Pending"),
        ("accepted", "Accepted"),
        ("revoked", "Revoked"),
        ("expired", "Expired"),
    ]

    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="invitations")
    email = models.EmailField()
    role = models.ForeignKey(Role, on_delete=models.SET_NULL, null=True, blank=True, related_name="+")
    donor_company = models.ForeignKey(
        "companies.Company", on_delete=models.SET_NULL, null=True, blank=True, related_name="+",
        help_text="Set for donor-portal invites instead of `role` — acceptance creates a donor account (organization_role='donor') linked to this company, with no RBAC role.",
    )
    donor_role = models.CharField(
        max_length=10, choices=[("admin", "Donor Admin"), ("viewer", "Donor Viewer")], blank=True, default="",
        help_text="For donor-portal invites: the donor_role the created account gets. Empty for staff invites.",
    )
    token = models.CharField(max_length=64, unique=True, default=_invitation_token, editable=False)
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default="pending", db_index=True)
    invited_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="+")
    expires_at = models.DateTimeField()
    created_at = models.DateTimeField(auto_now_add=True)
    accepted_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        db_table = "invitations"
        ordering = ["-created_at"]
        constraints = [
            models.UniqueConstraint(
                fields=["organization", "email"],
                condition=models.Q(status="pending"),
                name="unique_pending_invite_per_org_email",
            )
        ]

    def save(self, *args, **kwargs):
        if not self.expires_at:
            self.expires_at = timezone.now() + timezone.timedelta(days=7)
        super().save(*args, **kwargs)

    def __str__(self):
        return f"{self.email} -> {self.organization.name} ({self.status})"


class DepartmentMembership(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="department_memberships")
    department = models.ForeignKey("companies.Department", on_delete=models.CASCADE, related_name="access_memberships")
    role = models.CharField(max_length=20, choices=[("staff", "Staff"), ("manager", "Department Manager")], default="staff")
    assigned_at = models.DateTimeField(auto_now_add=True)
    assigned_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="+")
    is_active = models.BooleanField(default=True, db_index=True)
    expires_at = models.DateTimeField(null=True, blank=True, db_index=True)
    suspended_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        db_table = "rbac_department_memberships"
        ordering = ["department__name", "user__email"]
        constraints = [models.UniqueConstraint(fields=["user", "department"], name="unique_user_department_membership")]


class AccessAuditLog(models.Model):
    event_type = models.CharField(max_length=60, db_index=True)
    object_type = models.CharField(max_length=60)
    object_id = models.CharField(max_length=80, blank=True)
    actor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name="+")
    target_user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name="access_audit_events")
    previous_value = models.JSONField(default=dict, blank=True)
    new_value = models.JSONField(default=dict, blank=True)
    reason = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)

    class Meta:
        db_table = "rbac_access_audit_logs"
        ordering = ["-created_at"]


class AccessChangeRequest(models.Model):
    target_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="access_change_requests")
    old_department = models.ForeignKey("companies.Department", on_delete=models.SET_NULL, null=True, blank=True, related_name="outgoing_access_requests")
    new_department = models.ForeignKey("companies.Department", on_delete=models.PROTECT, related_name="incoming_access_requests")
    proposed_changes = models.JSONField(default=dict)
    reason = models.TextField(blank=True)
    status = models.CharField(max_length=20, choices=[("pending", "Pending"), ("approved", "Approved"), ("rejected", "Rejected")], default="pending", db_index=True)
    requested_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name="requested_access_changes")
    reviewed_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="reviewed_access_changes")
    requested_at = models.DateTimeField(auto_now_add=True)
    reviewed_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        db_table = "rbac_access_change_requests"
        ordering = ["-requested_at"]


class Tag(models.Model):
    """Tags for organizing and filtering items."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="tags")
    slug = models.SlugField(max_length=50, blank=True, null=True)
    name = models.CharField(max_length=50)
    color = models.CharField(max_length=7, default="#6b7280")
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "tags"
        ordering = ["name"]
        constraints = [
            models.UniqueConstraint(fields=["organization", "name"], name="unique_tag_name_per_org"),
            models.UniqueConstraint(fields=["organization", "slug"], name="unique_tag_slug_per_org"),
        ]

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if not self.slug and self.name:
            self.slug = self.name.lower().replace(" ", "-")
        super().save(*args, **kwargs)


class Attachment(models.Model):
    """File attachments for various entities."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="attachments")
    file = models.FileField(upload_to=tenant_upload_path("attachments"))
    name = models.CharField(max_length=255)
    size = models.IntegerField()
    mime_type = models.CharField(max_length=100)
    uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name="attachments")
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "attachments"
        ordering = ["-created_at"]


class Comment(models.Model):
    """Comments on tasks, projects, events, funds, etc. — attaches to any
    model via a generic relation (content_type + object_id)."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.CharField(max_length=64)
    content_object = GenericForeignKey("content_type", "object_id")
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="comments")
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "comments"
        ordering = ["created_at"]
        indexes = [models.Index(fields=["content_type", "object_id"])]


class Notification(models.Model):
    """User notifications."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notifications")
    title = models.CharField(max_length=255)
    message = models.TextField()
    is_read = models.BooleanField(default=False)
    link = models.CharField(max_length=500, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "notifications"
        ordering = ["-created_at"]


class AuditLog(models.Model):
    """Audit trail for tracking changes."""
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    organization = models.ForeignKey("companies.Organization", on_delete=models.CASCADE, related_name="audit_logs")
    user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    action = models.CharField(max_length=50)
    model_name = models.CharField(max_length=100)
    object_id = models.UUIDField()
    changes = models.JSONField(default=dict)
    ip_address = models.GenericIPAddressField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "audit_logs"
        ordering = ["-created_at"]
        indexes = [models.Index(fields=["model_name", "object_id"])]


class AppConfig(models.Model):
    """Singleton system configuration — AI providers and outgoing email.

    Stores API keys in the database so admins can manage them from Settings
    instead of editing env files. Falls back to env vars when a field is blank.
    """
    AI_PROVIDERS = [
        ("deepseek", "DeepSeek"),
        ("minimax", "MiniMax M3"),
    ]

    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)

    # --- AI assistant ---
    ai_provider = models.CharField(max_length=20, choices=AI_PROVIDERS, default="deepseek")

    deepseek_api_key = models.CharField(max_length=255, blank=True)
    deepseek_base_url = models.CharField(max_length=255, blank=True, default="https://api.deepseek.com")
    deepseek_model = models.CharField(max_length=100, blank=True, default="deepseek-chat")

    minimax_api_key = models.CharField(max_length=255, blank=True)
    minimax_base_url = models.CharField(max_length=255, blank=True, default="https://api.minimax.io/v1")
    minimax_model = models.CharField(max_length=100, blank=True, default="MiniMax-M3")

    # --- Outgoing email (SMTP) ---
    email_host = models.CharField(max_length=255, blank=True)
    email_port = models.PositiveIntegerField(default=587)
    email_username = models.CharField(max_length=255, blank=True)
    email_password = models.CharField(max_length=255, blank=True)
    email_from = models.EmailField(blank=True)
    email_use_tls = models.BooleanField(default=True)

    # --- Calendar ---
    calendar_default_view = models.CharField(
        max_length=10,
        choices=[("week", "Week"), ("month", "Month")],
        default="week",
    )
    hr_department = models.ForeignKey("companies.Department", on_delete=models.SET_NULL, null=True, blank=True, related_name="+", help_text="Existing department whose managers hold HR authority.")
    procurement_department = models.ForeignKey("companies.Department", on_delete=models.SET_NULL, null=True, blank=True, related_name="+", help_text="Department whose active members hold organization-wide Procurement authority.")
    finance_department = models.ForeignKey("companies.Department", on_delete=models.SET_NULL, null=True, blank=True, related_name="+", help_text="Department whose active members hold organization-wide Finance authority.")

    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "app_config"

    def __str__(self):
        return "App Configuration"
