"""
Core application factories
"""

import json
import uuid

from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.utils import timezone

import factory.fuzzy
from faker import Faker

from core import enums, models
from core.enums import UserEventTypeChoices
from core.mda.utils import generate_mime_id
from core.services.blob_gc import upload_and_reserve_blob

fake = Faker()


class UserFactory(factory.django.DjangoModelFactory):
    """A factory to random users for testing purposes."""

    class Meta:
        model = models.User
        skip_postgeneration_save = True

    sub = factory.Sequence(lambda n: f"user{n!s}")
    email = factory.Faker("email")
    full_name = factory.Faker("name")
    language = factory.fuzzy.FuzzyChoice([lang[0] for lang in settings.LANGUAGES])
    password = make_password("password")


class ParentNodeFactory(factory.declarations.ParameteredAttribute):
    """Custom factory attribute for setting the parent node."""

    def generate(self, step, params):
        """
        Generate a parent node for the factory.

        This method is invoked during the factory's build process to determine the parent
        node of the current object being created. If `params` is provided, it uses the factory's
        metadata to recursively create or fetch the parent node. Otherwise, it returns `None`.
        """
        if not params:
            return None
        subfactory = step.builder.factory_meta.factory
        return step.recurse(subfactory, params)


class MailDomainFactory(factory.django.DjangoModelFactory):
    """A factory to random mail domains for testing purposes, ensuring uniqueness."""

    class Meta:
        model = models.MailDomain

    name = factory.Sequence(lambda n: f"example{n}.com")


class MailboxFactory(factory.django.DjangoModelFactory):
    """A factory to random mailboxes for testing purposes."""

    class Meta:
        model = models.Mailbox
        skip_postgeneration_save = True

    domain = factory.SubFactory(MailDomainFactory)
    local_part = factory.Sequence(lambda n: f"john.doe{n!s}")

    @factory.post_generation
    def users_read(self, create, users, **kwargs):
        """
        Optionally assign users with read access to this mailbox.
        Usage: MailboxFactory(users_read=[user1, user2])
        """
        if not create or not users:
            return
        for user in users:
            models.MailboxAccess.objects.create(
                mailbox=self, user=user, role=models.MailboxRoleChoices.VIEWER
            )

    @factory.post_generation
    def users_admin(self, create, users, **kwargs):
        """
        Optionally assign users with admin access to this mailbox.
        Usage: MailboxFactory(users_admin=[user1, user2])
        """
        if not create or not users:
            return
        for user in users:
            models.MailboxAccess.objects.create(
                mailbox=self,
                user=user,
                role=models.MailboxRoleChoices.ADMIN,
            )


class MailboxAccessFactory(factory.django.DjangoModelFactory):
    """A factory to random mailbox accesses for testing purposes."""

    class Meta:
        model = models.MailboxAccess

    mailbox = factory.SubFactory(MailboxFactory)
    user = factory.SubFactory(UserFactory)
    role = factory.fuzzy.FuzzyChoice(
        [role[0] for role in models.MailboxRoleChoices.choices]
    )


class MailDomainAccessFactory(factory.django.DjangoModelFactory):
    """A factory to random mail domain accesses for testing purposes."""

    class Meta:
        model = models.MailDomainAccess

    maildomain = factory.SubFactory(MailDomainFactory)
    user = factory.SubFactory(UserFactory)
    role = factory.fuzzy.FuzzyChoice(
        [role[0] for role in models.MailDomainAccessRoleChoices.choices]
    )


class ThreadFactory(factory.django.DjangoModelFactory):
    """A factory to random threads for testing purposes."""

    class Meta:
        model = models.Thread

    subject = factory.Faker("sentence")
    snippet = factory.Faker("text")


class ThreadAccessFactory(factory.django.DjangoModelFactory):
    """A factory to random thread accesses for testing purposes."""

    class Meta:
        model = models.ThreadAccess

    thread = factory.SubFactory(ThreadFactory)
    mailbox = factory.SubFactory(MailboxFactory)
    role = factory.fuzzy.FuzzyChoice(
        [role[0] for role in models.ThreadAccessRoleChoices.choices]
    )


class ThreadEventFactory(factory.django.DjangoModelFactory):
    """A factory to create thread events for testing purposes.

    Mirrors the side effects that production code attaches to the
    creation of these events — namely the matching ``UserEvent`` rows —
    so existing tests that exercise the resulting state via the factory
    keep working without having to call ``thread_events_service``
    explicitly. Production code reaches the same effect by calling the
    service from viewsets / admin; this hook is the test-only
    equivalent.
    """

    class Meta:
        model = models.ThreadEvent
        skip_postgeneration_save = True

    thread = factory.SubFactory(ThreadFactory)
    type = "im"
    data = factory.LazyAttribute(lambda o: {"content": fake.sentence()})
    author = factory.SubFactory(UserFactory)

    # pylint: disable=protected-access,no-member
    # ``self.data`` is a real dict at runtime (factory_boy resolves
    # ``LazyAttribute`` before post_generation hooks fire) but pylint sees
    # the class-level descriptor and reports ``no-member``. Reaching into
    # the private service helpers is intentional — see the docstring above.
    @factory.post_generation
    def _sync_user_events(self, create, _extracted, **_kwargs):
        # pylint: disable=import-outside-toplevel
        from core.services import thread_events as thread_events_service

        if not create:
            return
        data = self.data or {}
        if self.type == enums.ThreadEventTypeChoices.IM:
            if data.get("mentions"):
                thread_events_service.sync_im_mentions(thread_event=self)
        elif self.type == enums.ThreadEventTypeChoices.ASSIGN:
            assignees = data.get("assignees") or []
            if assignees:
                thread_events_service._create_user_event_assigns(  # noqa: SLF001
                    self, self.thread, assignees
                )
        elif self.type == enums.ThreadEventTypeChoices.UNASSIGN:
            assignees = data.get("assignees") or []
            if assignees:
                thread_events_service._delete_user_event_assigns(  # noqa: SLF001
                    self.thread, assignees, context=str(self.id)
                )


class UserEventFactory(factory.django.DjangoModelFactory):
    """A factory to create user events for testing purposes."""

    class Meta:
        model = models.UserEvent

    user = factory.SubFactory(UserFactory)
    thread = factory.SubFactory(ThreadFactory)
    thread_event = factory.SubFactory(
        ThreadEventFactory,
        thread=factory.SelfAttribute("..thread"),
    )
    type = UserEventTypeChoices.MENTION


class ContactFactory(factory.django.DjangoModelFactory):
    """A factory to random contacts for testing purposes."""

    class Meta:
        model = models.Contact
        django_get_or_create = ("email", "mailbox")

    name = factory.Faker("name")
    email = factory.Faker("email")
    mailbox = factory.SubFactory(MailboxFactory)


class MessageFactory(factory.django.DjangoModelFactory):
    """A factory to random messages for testing purposes."""

    class Meta:
        model = models.Message

    thread = factory.SubFactory(ThreadFactory)
    subject = factory.Faker("sentence")
    sender = factory.SubFactory(ContactFactory)
    created_at = factory.LazyAttribute(lambda o: timezone.now())
    mime_id = factory.LazyFunction(lambda: generate_mime_id("test.example.com"))

    @factory.post_generation
    def raw_mime(self, create, extracted, **kwargs):
        """
        Create a blob with raw MIME content when raw_mime is provided.
        Usage: MessageFactory(raw_mime=b"raw email content")
        """
        if not create or not extracted:
            return

        self.blob = models.Blob.objects.create_blob(  # pylint: disable=attribute-defined-outside-init
            content=extracted,
            content_type="message/rfc822",
        )
        self.save()


class MessageRecipientFactory(factory.django.DjangoModelFactory):
    """A factory to random message recipients for testing purposes."""

    class Meta:
        model = models.MessageRecipient

    message = factory.SubFactory(MessageFactory)
    contact = factory.SubFactory(ContactFactory)
    type = factory.fuzzy.FuzzyChoice(
        [type[0] for type in models.MessageRecipientTypeChoices.choices]
    )


class LabelFactory(factory.django.DjangoModelFactory):
    """Factory for creating test labels."""

    name = factory.Sequence(lambda n: f"Label {n}")
    mailbox = factory.SubFactory(MailboxFactory)

    class Meta:
        model = models.Label

    @factory.post_generation
    def threads(self, create, extracted, **kwargs):
        """Add threads to the label if provided."""
        if not create or not extracted:
            return

        if isinstance(extracted, (list, tuple)):
            for thread in extracted:
                self.threads.add(thread)


class AttachmentFactory(factory.django.DjangoModelFactory):
    """Factory for ``Attachment`` rows.

    Each Attachment belongs to exactly one ``Message`` via FK, so
    ``message=`` is required. Tests that don't care about the
    specific draft can let the factory autogenerate one.
    """

    class Meta:
        model = models.Attachment

    mailbox = factory.SubFactory(MailboxFactory)
    name = factory.Sequence(lambda n: f"attachment{n}.txt")
    blob_size = 1500

    @factory.lazy_attribute
    def blob(self):
        """Create a blob with specified size for the attachment."""
        content = b"x" * self.blob_size
        return BlobFactory(mailbox=self.mailbox, content=content)

    @factory.lazy_attribute
    def message(self):
        """Default to a fresh draft Message owned by ``mailbox``.

        Tests that already have a draft handy should pass it
        explicitly: ``AttachmentFactory(message=draft, ...)``.
        """
        sender_email = f"{self.mailbox.local_part}@{self.mailbox.domain.name}"
        sender, _ = models.Contact.objects.get_or_create(
            email=sender_email,
            mailbox=self.mailbox,
            defaults={"name": self.mailbox.local_part, "email": sender_email},
        )
        thread = ThreadFactory()
        ThreadAccessFactory(
            thread=thread,
            mailbox=self.mailbox,
            role=enums.ThreadAccessRoleChoices.EDITOR,
        )
        return MessageFactory(
            thread=thread, sender=sender, is_draft=True, is_sender=True
        )

    @classmethod
    def _adjust_kwargs(cls, **kwargs):
        """
        Adjust the keyword arguments before passing them to the model.
        """
        # Remove blob_size from kwargs before passing to model
        kwargs = dict(kwargs)
        kwargs.pop("blob_size", None)
        return kwargs


class ChannelFactory(factory.django.DjangoModelFactory):
    """A factory to create channels for testing purposes.

    ``scope_level`` is derived from the FKs by default:
      - mailbox set → "mailbox" (default)
      - maildomain set → "maildomain"
      - user set → "user"
      - none of the above → "global"
    Callers can always override ``scope_level=`` explicitly.
    """

    class Meta:
        model = models.Channel

    name = factory.Sequence(lambda n: f"Test Channel {n}")
    type = factory.fuzzy.FuzzyChoice(["widget", "mta"])
    settings = factory.Dict({"config": {"enabled": True}})
    # Auto-mint a placeholder webhook signing secret for webhook channels
    # so tests don't all have to do it themselves — the dispatcher
    # fails-closed without one. Non-webhook channels keep an empty
    # encrypted_settings (matching production behaviour).
    encrypted_settings = factory.LazyAttribute(
        lambda o: {"secret": "whsec_factory_test"} if o.type == "webhook" else {}
    )
    mailbox = factory.SubFactory(MailboxFactory)
    maildomain = None
    user = None
    scope_level = factory.LazyAttribute(
        lambda o: (
            "mailbox"
            if o.mailbox is not None
            else "maildomain"
            if o.maildomain is not None
            else "user"
            if o.user is not None
            else "global"
        )
    )


def make_api_key_channel(
    *,
    scope_level=enums.ChannelScopeLevel.GLOBAL,
    scopes=(),
    mailbox=None,
    maildomain=None,
    user=None,
    name=None,
    extra_settings=None,
):
    """Create an api_key Channel and return ``(channel, plaintext)``.

    Single source of truth for tests that need a working api_key. Mints
    the secret via ``Channel.rotate_secret`` so the production helper is
    exercised on the same code path the DRF/admin flows use, and so the
    plaintext prefix stays consistent across production and tests.

    Returned plaintext is the only chance to capture it — the row's
    ``encrypted_settings.api_key_hashes`` only stores the SHA-256 digest.
    """
    channel = models.Channel(
        name=name or f"test-{uuid.uuid4().hex[:6]}",
        type=enums.ChannelTypes.API_KEY,
        scope_level=scope_level,
        mailbox=mailbox,
        maildomain=maildomain,
        user=user,
        settings={"scopes": list(scopes), **(extra_settings or {})},
    )
    plaintext = channel.rotate_secret(save=False)
    channel.save()
    return channel, plaintext


class BlobFactory(factory.django.DjangoModelFactory):
    """A factory to create blobs for testing purposes.

    Pass ``mailbox=X`` to simulate a JMAP upload (registers an upload
    reservation under that mailbox); omit it for plain server-side
    fixtures (no reservation, just dedup-create).
    """

    class Meta:
        model = models.Blob

    content = factory.LazyAttribute(lambda o: b"Blob content")
    content_type = factory.LazyAttribute(lambda o: "application/octet-stream")

    @classmethod
    def _create(cls, model_class, *args, **kwargs):
        content = kwargs.pop("content")
        content_type = kwargs.pop("content_type", "application/octet-stream")
        mailbox = kwargs.pop("mailbox", None)
        if mailbox is not None:
            return upload_and_reserve_blob(mailbox, content, content_type)
        return models.Blob.objects.create_blob(
            content=content, content_type=content_type
        )


class MessageTemplateFactory(factory.django.DjangoModelFactory):
    """A factory to create message templates for testing purposes."""

    class Meta:
        model = models.MessageTemplate
        skip_postgeneration_save = True

    name = factory.Sequence(lambda n: f"Template {n}")
    type = factory.fuzzy.FuzzyChoice(
        [
            choice[0]
            for choice in enums.MessageTemplateTypeChoices.choices
            if choice[0] != enums.MessageTemplateTypeChoices.AUTOREPLY
        ]
    )
    metadata = factory.LazyAttribute(
        lambda o: (
            {"schedule_type": "always"}
            if o.type == enums.MessageTemplateTypeChoices.AUTOREPLY
            else {}
        )
    )

    @classmethod
    def _create(cls, model_class, *args, **kwargs):
        """Override _create to handle content blob creation."""
        # Extract content-related kwargs

        html_body = kwargs.pop("html_body", fake.sentence(nb_words=10))
        text_body = kwargs.pop("text_body", fake.text(max_nb_chars=100))
        raw_body = kwargs.pop("raw_body", {"key": "value"})

        # Create the template first
        template = super()._create(model_class, *args, **kwargs)

        # Only create content blob if one wasn't provided
        if not template.blob:
            content = {"html": html_body, "text": text_body, "raw": raw_body}

            # Create content blob using the mailbox if available, otherwise use maildomain's first mailbox
            mailbox = template.mailbox or (
                template.maildomain.mailbox_set.first() if template.maildomain else None
            )

            template.blob = BlobFactory(
                content=json.dumps(content).encode("utf-8"),
                content_type="application/json",
                mailbox=mailbox,
                maildomain=template.maildomain if not mailbox else None,
            )
            template.save()

        return template
