import secrets

import rules
import urlman
from auditlog.registry import auditlog
from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import send_mail
from django.core.validators import RegexValidator
from django.db import models, transaction
from django.http import HttpRequest
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled

from servala.core import rules as perms
from servala.core.models.mixins import ServalaModelMixin
from servala.core.odoo import CLIENT


class Organization(ServalaModelMixin, models.Model):
    name = models.CharField(
        max_length=32,
        verbose_name=_("Name"),
        validators=[
            RegexValidator(
                regex=r"^[A-Za-z0-9\s]+$",
                message=_(
                    "Organization name can only contain letters, numbers, and spaces."
                ),
            )
        ],
    )
    # The namespace is generated as "org-{id}" in accordance with RFC 1035 Label Names.
    # It is nullable as we need to write to the database in order to read the ID, but should
    # not be null in practical use.
    namespace = models.CharField(
        max_length=20,
        verbose_name=_("Kubernetes Namespace"),
        null=True,
        unique=True,
        help_text=_("This namespace will be used for all Kubernetes resources."),
    )

    billing_entity = models.ForeignKey(
        to="BillingEntity",
        on_delete=models.PROTECT,
        related_name="organizations",
        verbose_name=_("Billing entity"),
        null=True,  # TODO: billing entity should be required
    )
    origin = models.ForeignKey(
        to="OrganizationOrigin",
        on_delete=models.PROTECT,
        related_name="organizations",
        verbose_name=_("Origin"),
    )

    members = models.ManyToManyField(
        to="core.User",
        through="OrganizationMembership",
        related_name="organizations",
        verbose_name=_("Members"),
    )
    limit_osb_services = models.ManyToManyField(
        to="Service",
        related_name="+",
        verbose_name=_("Services activated from OSB"),
        blank=True,
    )

    odoo_sale_order_id = models.IntegerField(
        null=True, blank=True, verbose_name=_("Odoo Sale Order ID")
    )
    odoo_sale_order_name = models.CharField(
        max_length=100, null=True, blank=True, verbose_name=_("Odoo Sale Order Name")
    )
    osb_guid = models.CharField(
        max_length=100,
        null=True,
        blank=True,
        verbose_name=_("OSB GUID"),
        help_text=_(
            "Open Service Broker GUID, used for organizations created via OSB API"
        ),
    )

    class urls(urlman.Urls):
        base = "/org/{self.slug}/"
        details = "{base}details/"
        services = "{base}services/"
        instances = "{base}instances/"
        support = "{base}support/"

    @cached_property
    def slug(self):
        return f"{slugify(self.name)}-{self.id}"

    def get_absolute_url(self):
        return self.urls.base

    @property
    def has_inherited_billing_entity(self):
        return self.origin and self.billing_entity == self.origin.billing_entity

    @property
    def limit_cloudproviders(self):
        if self.origin:
            return self.origin.limit_cloudproviders.all()
        from servala.core.models import CloudProvider

        return CloudProvider.objects.none()

    def set_owner(self, user):
        with scopes_disabled():
            OrganizationMembership.objects.filter(user=user, organization=self).delete()
            OrganizationMembership.objects.create(
                user=user, organization=self, role=OrganizationRole.OWNER
            )

    def add_support_message(self, message):
        support_message = _(
            "Need help? We're happy to help via the <a href='{support_url}'>support form</a>."
        ).format(support_url=self.urls.support)
        return mark_safe(
            f'{message} <br /><i class="bi bi-person-raised-hand"></i> {support_message}'
        )

    @classmethod
    @transaction.atomic
    def create_organization(cls, instance, owner=None):
        try:
            instance.origin
        except Exception:
            instance.origin = OrganizationOrigin.objects.get(
                pk=settings.SERVALA_DEFAULT_ORIGIN
            )
        instance.save()
        if owner:
            instance.set_owner(owner)

        if instance.origin and instance.origin.default_odoo_sale_order_id:
            sale_order_id = instance.origin.default_odoo_sale_order_id
            sale_order_data = CLIENT.search_read(
                model="sale.order",
                domain=[["id", "=", sale_order_id]],
                fields=["name"],
                limit=1,
            )

            instance.odoo_sale_order_id = sale_order_id
            if sale_order_data:
                instance.odoo_sale_order_name = sale_order_data[0]["name"]
            instance.save(update_fields=["odoo_sale_order_id", "odoo_sale_order_name"])
        elif (
            instance.billing_entity.odoo_company_id
            and instance.billing_entity.odoo_invoice_id
        ):
            payload = {
                "partner_id": instance.billing_entity.odoo_company_id,
                "partner_invoice_id": instance.billing_entity.odoo_invoice_id,
                "state": "sale",
                "client_order_ref": f"Servala (Organization: {instance.name})",
                "internal_note": "auto-generated by Servala Portal",
            }
            sale_order_id = CLIENT.execute("sale.order", "create", [payload])

            sale_order_data = CLIENT.search_read(
                model="sale.order",
                domain=[["id", "=", sale_order_id]],
                fields=["name"],
                limit=1,
            )

            instance.odoo_sale_order_id = sale_order_id
            if sale_order_data:
                instance.odoo_sale_order_name = sale_order_data[0]["name"]
            instance.save(update_fields=["odoo_sale_order_id", "odoo_sale_order_name"])

        return instance

    def get_visible_services(self):
        from servala.core.models import Service

        queryset = Service.objects.all()
        if self.limit_osb_services.exists():
            queryset = self.limit_osb_services.all()
        if self.limit_cloudproviders.exists():
            queryset = queryset.filter(
                offerings__provider__in=self.limit_cloudproviders
            ).distinct()
        return queryset.prefetch_related(
            "offerings", "offerings__provider"
        ).select_related("category")

    def get_deactivated_services(self):
        from servala.core.models import Service

        if not self.limit_osb_services.exists():
            return Service.objects.none()

        queryset = Service.objects.select_related("category")
        if self.limit_cloudproviders.exists():
            queryset = queryset.filter(
                offerings__provider__in=self.limit_cloudproviders
            ).distinct()
        queryset = queryset.exclude(id__in=self.limit_osb_services.all())
        return queryset.prefetch_related("offerings", "offerings__provider")

    class Meta:
        verbose_name = _("Organization")
        verbose_name_plural = _("Organizations")
        rules_permissions = {
            "view": rules.is_staff | perms.is_organization_member,
            "change": rules.is_staff | perms.is_organization_admin,
            "delete": rules.is_staff | perms.is_organization_owner,
            "add": rules.is_authenticated,
        }

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        if not self.namespace:
            # Set namespace after initial save to ensure we have an ID
            self.namespace = f"org-{self.pk}"
            self.save(update_fields=["namespace"])


class BillingEntity(ServalaModelMixin, models.Model):
    """
    Every organization has a billing entity, though billing entities
    may be shared across different organizations.
    """

    name = models.CharField(max_length=100, verbose_name=_("Name"))
    description = models.TextField(blank=True, verbose_name=_("Description"))
    erp_reference = models.CharField(
        max_length=100, blank=True, verbose_name=_("ERP reference")
    )

    # Odoo IDs are nullable for creation, should never be null in practice
    # The company ID points at a record of type res.partner with is_company=True
    # The invoice ID points at a record of type res.partner with is_company=False,
    # type=invoice, parent_id=company_id (the invoice address).
    odoo_company_id = models.IntegerField(null=True)
    odoo_invoice_id = models.IntegerField(null=True)

    class Meta:
        verbose_name = _("Billing entity")
        verbose_name_plural = _("Billing entities")

    def __str__(self):
        return self.name

    @classmethod
    @transaction.atomic
    def create_from_data(cls, name, odoo_data):
        """
        Creates a BillingEntity and corresponding Odoo records.

        This method creates a `res.partner` record in Odoo with `is_company=True`
        for the main company, and another `res.partner` record with `is_company=False`
        and `type='invoice'` (linked via `parent_id` to the first record) for the
        invoice address. The IDs of these Odoo records are stored in the BillingEntity.

        Args:
            odoo_data (dict): A dictionary containing the data for creating
                              the BillingEntity and Odoo records.

        Expected keys in `odoo_data`:
            - `invoice_street` (str): Street for the invoice address.
            - `invoice_street2` (str): Second line of street address for the invoice address.
            - `invoice_city` (str): City for the invoice address.
            - `invoice_zip` (str): ZIP/Postal code for the invoice address.
            - `invoice_country` (int): Odoo `res.country` ID for the invoice address country.
            - `invoice_email` (str): Email address for the invoice contact.
            - `invoice_phone` (str): Phone number for the invoice contact.
        """
        instance = cls.objects.create(name=name)
        company_payload = {
            "name": odoo_data.get("company_name", name),
            "is_company": True,
        }
        company_id = CLIENT.execute("res.partner", "create", [company_payload])
        instance.odoo_company_id = company_id

        invoice_address_payload = {
            "name": name,
            "is_company": False,
            "type": "invoice",
            "parent_id": company_id,
        }
        invoice_optional_fields = {
            "street": odoo_data.get("invoice_street"),
            "street2": odoo_data.get("invoice_street2"),
            "city": odoo_data.get("invoice_city"),
            "zip": odoo_data.get("invoice_zip"),
            "country_id": odoo_data.get("invoice_country"),
            "email": odoo_data.get("invoice_email"),
        }
        invoice_address_payload.update(
            {k: v for k, v in invoice_optional_fields.items() if v is not None}
        )

        invoice_address_id = CLIENT.execute(
            "res.partner", "create", [invoice_address_payload]
        )
        instance.odoo_invoice_id = invoice_address_id

        instance.save(update_fields=["odoo_company_id", "odoo_invoice_id"])
        return instance

    @classmethod
    @transaction.atomic
    def create_from_id(cls, name, odoo_id):
        parent_data = CLIENT.search_read(
            model="res.partner",
            domain=[["id", "=", odoo_id]],
            fields=["parent_id"],
            limit=1,
        )
        # Data validation: If the data is not as expected, we just return None,
        # rather than raising an exception, for now.
        if not parent_data:
            return
        if not (parent_info := parent_data[0].get("parent_id")):
            return
        if not isinstance(parent_info, (list, tuple)) or not len(parent_info) > 0:
            # parent_info is a tuple of the parent’s ID and name
            return

        instance = cls.objects.create(
            name=name, odoo_invoice_id=odoo_id, odoo_company_id=parent_info[0]
        )
        return instance

    @cached_property
    def odoo_data(self):
        data = {
            "company": None,
            "invoice_address": None,
        }

        company_fields = ["name", "is_company"]
        invoice_address_fields = [
            "name",
            "is_company",
            "type",
            "parent_id",
            "street",
            "street2",
            "city",
            "zip",
            "country_id",
            "email",
        ]

        if self.odoo_company_id:
            company_records = CLIENT.search_read(
                model="res.partner",
                domain=[["id", "=", self.odoo_company_id]],
                fields=company_fields,
                limit=1,
            )
            if company_records:
                data["company"] = company_records[0]

        if self.odoo_invoice_id:
            invoice_address_records = CLIENT.search_read(
                model="res.partner",
                domain=[["id", "=", self.odoo_invoice_id]],
                fields=invoice_address_fields,
                limit=1,
            )
            if invoice_address_records:
                data["invoice_address"] = invoice_address_records[0]

        return data


class InvoiceGroupingChoice(models.TextChoices):
    BY_SERVICE = "by_service", _("By Service")
    BY_ORGANIZATION = "by_organization", _("By Organization")


class OrganizationOrigin(ServalaModelMixin, models.Model):
    """
    Every organization has an origin, though origins may be
    shared across different organizations. The default origin
    (by PK) is configured in the settings as SERVALA_DEFAULT_ORIGIN.
    """

    name = models.CharField(max_length=100, verbose_name=_("Name"))
    description = models.TextField(blank=True, verbose_name=_("Description"))
    billing_entity = models.ForeignKey(
        to="BillingEntity",
        on_delete=models.PROTECT,
        related_name="origins",
        verbose_name=_("Billing entity"),
        help_text=_(
            "If set, this billing entity will be used on new organizations with this origin."
        ),
        null=True,
        blank=True,
    )
    limit_cloudproviders = models.ManyToManyField(
        to="CloudProvider",
        related_name="+",
        verbose_name=_("Limit to these Cloud providers"),
        blank=True,
        help_text=_(
            "If set, all organizations with this origin will be limited to these cloud providers."
        ),
    )
    default_odoo_sale_order_id = models.IntegerField(
        null=True,
        blank=True,
        verbose_name=_("Default Odoo Sale Order ID"),
        help_text=_(
            "If set, this sale order will be used for new organizations with this origin."
        ),
    )
    hide_billing_address = models.BooleanField(
        default=False,
        verbose_name=_("Hide Billing Address"),
        help_text=_(
            "If enabled, the billing address will not be shown in the organization details view."
        ),
    )
    billing_message = models.TextField(
        blank=True,
        verbose_name=_("Billing Message"),
        help_text=_(
            "Optional message to display instead of billing address (e.g., 'You will be invoiced by Exoscale')."
        ),
    )
    invoice_grouping = models.CharField(
        max_length=20,
        choices=InvoiceGroupingChoice.choices,
        default=InvoiceGroupingChoice.BY_SERVICE,
        verbose_name=_("Invoice Line Item Grouping"),
        help_text=_("Determines how service instances are grouped on invoices. "),
    )

    class Meta:
        verbose_name = _("Organization origin")
        verbose_name_plural = _("Organization origins")

    def __str__(self):
        return self.name


class OrganizationRole(models.TextChoices):
    MEMBER = "member", _("Member")
    ADMIN = "admin", _("Administrator")
    OWNER = "owner", _("Owner")


class OrganizationMembership(ServalaModelMixin, models.Model):
    """Through-model for the many-to-many relationship between organizations and users."""

    user = models.ForeignKey(
        to="core.User",
        on_delete=models.CASCADE,
        related_name="organization_memberships",
        verbose_name=_("User"),
    )
    organization = models.ForeignKey(
        to=Organization,
        on_delete=models.CASCADE,
        related_name="memberships",
        verbose_name=_("Organization"),
    )
    date_joined = models.DateTimeField(
        verbose_name=_("Date joined"), auto_now_add=True, editable=False
    )
    role = models.CharField(
        max_length=20,
        choices=OrganizationRole.choices,
        default=OrganizationRole.MEMBER,
        verbose_name=_("Role"),
    )

    objects = ScopedManager(organization="organization")

    class Meta:
        verbose_name = _("Organization membership")
        verbose_name_plural = _("Organization memberships")

    def __str__(self):
        return f"{self.user} in {self.organization} as {self.role}"


class OrganizationInvitation(ServalaModelMixin, models.Model):
    organization = models.ForeignKey(
        to=Organization,
        on_delete=models.CASCADE,
        related_name="invitations",
        verbose_name=_("Organization"),
    )
    email = models.EmailField(verbose_name=_("Email address"))
    role = models.CharField(
        max_length=20,
        choices=OrganizationRole.choices,
        default=OrganizationRole.MEMBER,
        verbose_name=_("Role"),
    )
    secret = models.CharField(
        max_length=64,
        unique=True,
        editable=False,
        verbose_name=_("Secret token"),
    )
    created_by = models.ForeignKey(
        to="core.User",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="created_invitations",
        verbose_name=_("Created by"),
    )
    accepted_by = models.ForeignKey(
        to="core.User",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="accepted_invitations",
        verbose_name=_("Accepted by"),
    )
    accepted_at = models.DateTimeField(
        null=True, blank=True, verbose_name=_("Accepted at")
    )

    class urls(urlman.Urls):
        accept = "/invitations/{self.secret}/accept/"
        delete = "{self.organization.urls.details}invitations/{self.pk}/delete/"

    class Meta:
        verbose_name = _("Organization invitation")
        verbose_name_plural = _("Organization invitations")

    def __str__(self):
        return f"Invitation for {self.email} to {self.organization}"

    def save(self, *args, **kwargs):
        if not self.secret:
            self.secret = secrets.token_urlsafe(48)
        super().save(*args, **kwargs)

    @property
    def is_accepted(self):
        # We check both accepted_by and accepted_at to avoid a deleted user
        # freeing up an invitation
        return bool(self.accepted_by or self.accepted_at)

    @property
    def can_be_accepted(self):
        return not self.is_accepted

    def send_invitation_email(self, request=None):
        subject = _("You're invited to join {organization} on Servala").format(
            organization=self.organization.name
        )

        if request:
            invitation_url = request.build_absolute_uri(self.urls.accept)
            organization_url = request.build_absolute_uri(self.organization.urls.base)
        else:
            fake_request = HttpRequest()
            fake_request.META["SERVER_NAME"] = get_current_site(None).domain
            fake_request.META["SERVER_PORT"] = "443"
            fake_request.META["wsgi.url_scheme"] = "https"
            invitation_url = fake_request.build_absolute_uri(self.urls.accept)
            organization_url = fake_request.build_absolute_uri(
                self.organization.urls.base
            )

        message = _(
            """Hello,

You have been invited to join the organization "{organization}" on Servala Portal as a {role}.

To accept this invitation, please click the link below:
{invitation_url}

Once you accept, you'll be able to access the organization at:
{organization_url}

Best regards,
The Servala Team"""
        ).format(
            organization=self.organization.name,
            role=self.get_role_display(),
            invitation_url=invitation_url,
            organization_url=organization_url,
        )

        send_mail(
            subject=subject,
            message=message,
            from_email=settings.EMAIL_DEFAULT_FROM,
            recipient_list=[self.email],
            fail_silently=False,
        )


auditlog.register(OrganizationInvitation, serialize_data=True)
auditlog.register(OrganizationMembership, serialize_data=True)
