# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

from collections import defaultdict
from urllib.parse import urlparse

from django.conf import settings
from django.db import transaction
from rest_framework import serializers

from code_review_backend.issues.models import (
    LEVEL_ERROR,
    Diff,
    Issue,
    IssueLink,
    Repository,
    Revision,
)


class RepositorySerializer(serializers.ModelSerializer):
    """
    Serialize a Repository
    """

    class Meta:
        model = Repository
        fields = ("id", "slug", "url")


class RepositoryGetOrCreateField(serializers.SlugRelatedField):
    help_text = (
        "Get or create a repository. URL must match allowed hosts from settings."
    )
    default_error_messages = {
        **serializers.SlugRelatedField.default_error_messages,
        "invalid_url": "Repository URL must match hg.mozilla.org.",
    }
    queryset = Repository.objects.all()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs, slug_field="url")

    def to_internal_value(self, url):
        parsed = urlparse(url)
        if parsed.netloc not in settings.ALLOWED_REPOSITORY_HOSTS:
            try:
                return self.get_queryset().get(url=url)
            except Repository.DoesNotExist:
                self.fail("invalid_url")
            except (TypeError, ValueError):
                self.fail("invalid")
                return
        try:
            repo, _ = self.get_queryset().get_or_create(
                url=url, defaults={"slug": parsed.path.lstrip("/")}
            )
            return repo
        except (TypeError, ValueError):
            self.fail("invalid")


class RevisionSerializer(serializers.ModelSerializer):
    """
    Serialize a Revision in a Repository
    """

    base_repository = RepositoryGetOrCreateField()
    head_repository = RepositoryGetOrCreateField()
    diffs_url = serializers.HyperlinkedIdentityField(
        view_name="revision-diffs-list", lookup_url_kwarg="revision_id"
    )
    issues_bulk_url = serializers.HyperlinkedIdentityField(
        view_name="revision-issues-bulk", lookup_url_kwarg="revision_id"
    )
    phabricator_url = serializers.URLField(read_only=True)
    phabricator_id = serializers.IntegerField(
        required=False,
        allow_null=True,
        min_value=1,
        max_value=2147483647,
    )

    class Meta:
        model = Revision
        fields = (
            "id",
            "base_repository",
            "head_repository",
            "base_changeset",
            "head_changeset",
            "phabricator_id",
            "phabricator_phid",
            "title",
            "bugzilla_id",
            "diffs_url",
            "issues_bulk_url",
            "phabricator_url",
        )


class RevisionLightSerializer(serializers.ModelSerializer):
    """
    Serialize a Revision in a Diff light serializer
    """

    base_repository = RepositoryGetOrCreateField()
    head_repository = RepositoryGetOrCreateField()
    phabricator_url = serializers.URLField(read_only=True)

    class Meta:
        model = Revision
        fields = (
            "id",
            "phabricator_id",
            "base_repository",
            "head_repository",
            "base_changeset",
            "head_changeset",
            "phabricator_id",
            "title",
            "bugzilla_id",
            "phabricator_url",
        )


class DiffSerializer(serializers.ModelSerializer):
    """
    Serialize a Diff in a Revision
    Used for full management
    """

    repository = serializers.SlugRelatedField(
        queryset=Repository.objects.all(), slug_field="url"
    )
    issues_url = serializers.HyperlinkedIdentityField(
        view_name="issues-list", lookup_url_kwarg="diff_id"
    )

    class Meta:
        model = Diff
        fields = (
            "id",
            "phid",
            "review_task_id",
            "repository",
            "mercurial_hash",
            "issues_url",
        )


class DiffLightSerializer(serializers.ModelSerializer):
    """
    Serialize a Diff from an Issue in a check
    """

    repository = serializers.SlugRelatedField(
        queryset=Repository.objects.all(), slug_field="url"
    )

    revision = RevisionLightSerializer()

    class Meta:
        model = Diff
        fields = ("id", "repository", "revision")


class DiffFullSerializer(serializers.ModelSerializer):
    """
    Serialize a Diff with revision details
    This is used in a read only context
    """

    revision = RevisionSerializer(read_only=True)
    repository = RepositorySerializer(read_only=True)
    issues_url = serializers.HyperlinkedIdentityField(
        view_name="issues-list", lookup_url_kwarg="diff_id"
    )
    nb_issues = serializers.IntegerField(read_only=True)
    nb_issues_publishable = serializers.IntegerField(read_only=True)
    nb_warnings = serializers.IntegerField(read_only=True)
    nb_errors = serializers.IntegerField(read_only=True)

    class Meta:
        model = Diff
        fields = (
            "id",
            "revision",
            "phid",
            "review_task_id",
            "repository",
            "mercurial_hash",
            "issues_url",
            "nb_issues",
            "nb_issues_publishable",
            "nb_warnings",
            "nb_errors",
            "created",
        )


class IssueSerializer(serializers.ModelSerializer):
    """
    Serialize an Issue in a Diff
    """

    publishable = serializers.BooleanField(read_only=True)
    check = serializers.CharField(source="analyzer_check", required=False)
    publishable = serializers.BooleanField(read_only=True)
    in_patch = serializers.BooleanField(
        source="issue_links__in_patch", allow_null=True, required=False
    )
    new_for_revision = serializers.BooleanField(
        source="issue_links__new_for_revision", allow_null=True, required=False
    )

    line = serializers.IntegerField(
        source="issue_links__line", allow_null=True, required=False
    )
    nb_lines = serializers.IntegerField(
        source="issue_links__nb_lines", allow_null=True, required=False
    )
    char = serializers.IntegerField(
        source="issue_links__char", allow_null=True, required=False
    )

    class Meta:
        model = Issue
        fields = (
            "id",
            "hash",
            "analyzer",
            "path",
            "level",
            "check",
            "message",
            # Attrs coming from IssueLink
            "publishable",
            "in_patch",
            "new_for_revision",
            "line",
            "nb_lines",
            "char",
        )


class IssueHashSerializer(serializers.ModelSerializer):
    """
    Serialize an Issue hash
    """

    class Meta:
        model = Issue
        fields = (
            "id",
            "hash",
        )
        read_only_fields = ("id", "hash")


class SingleIssueBulkSerializer(IssueSerializer):
    # Make hash non unique to avoid validation checks
    hash = serializers.CharField(max_length=32)


class IssueBulkSerializer(serializers.Serializer):
    diff_id = serializers.PrimaryKeyRelatedField(
        # Initialized depending on the revision used for the creation
        queryset=Diff.objects.none(),
        style={"base_template": "input.html"},
        required=False,
        allow_null=True,
    )
    issues = SingleIssueBulkSerializer(many=True)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not self.context.get("revision"):
            return
        self.fields["diff_id"].queryset = self.context["revision"].diffs.all()

    @transaction.atomic
    def create(self, validated_data):
        diff = validated_data.get("diff_id", None)
        link_attrs = defaultdict(list)
        # Separate attributes that are specific to the IssueLink M2M
        for issue in validated_data["issues"]:
            link_attrs[issue["hash"]].append(
                {
                    "new_for_revision": issue.pop(
                        "issue_links__new_for_revision", None
                    ),
                    "in_patch": issue.pop("issue_links__in_patch", None),
                    "line": issue.pop("issue_links__line", None),
                    "nb_lines": issue.pop("issue_links__nb_lines", None),
                    "char": issue.pop("issue_links__path", None),
                }
            )
        # Only create issues that do not exist yet
        Issue.objects.bulk_create(
            [Issue(**values) for values in validated_data["issues"]],
            ignore_conflicts=True,
        )

        # Retrieve issues to get existing IDs
        hashes = set(link_attrs.keys())
        known_issues = {i.hash: i for i in Issue.objects.filter(hash__in=hashes)}

        assert set(known_issues.keys()) == hashes, "Failed to create all issues"

        # Create all links, using DB conflicts
        links = IssueLink.objects.bulk_create(
            [
                IssueLink(
                    issue_id=known_issues[issue_hash].id,
                    diff=diff,
                    revision=self.context["revision"],
                    **link,
                )
                for issue_hash, links in link_attrs.items()
                for link in links
            ],
            ignore_conflicts=True,
        )

        # Endpoint expects Issue with specific attributes for re-serialization of links
        # TODO in treeherder: only expose hash & publishable in output
        output = []
        for issue_hash, links in link_attrs.items():
            for link in links:
                existing_issue = known_issues[issue_hash]

                # Set attributes for re-serialization
                output_link = {f"issue_links__{k}": v for k, v in link.items()}
                output_link.update(vars(existing_issue))
                output_link["publishable"] = (
                    link["in_patch"] and existing_issue.level == LEVEL_ERROR
                )

                output.append(output_link)

        return {
            "diff_id": diff,
            "issues": output,
        }


class IssueCheckSerializer(IssueSerializer):
    """
    Serialize an Issue with all the diffs where it has been found.
    Each diff is serialized with its revision's information.
    """

    diffs = DiffLightSerializer(many=True)

    class Meta:
        model = Issue
        fields = IssueSerializer.Meta.fields + ("diffs",)


class IssueCheckStatsSerializer(serializers.Serializer):
    """
    Serialize the usage statistics for each check encountered
    """

    # The view aggregates issues depending on their reference to a repository (via IssueLink M2M)
    repository = serializers.SlugField(
        source="issue_links__revision__head_repository__slug"
    )
    analyzer = serializers.CharField()
    check = serializers.CharField(source="analyzer_check")
    total = serializers.IntegerField()
    publishable = serializers.IntegerField(read_only=True, default=0)

    class Meta:
        model = Issue
        fields = IssueSerializer.Meta.fields + ("repositories",)


class HistoryPointSerializer(serializers.Serializer):
    """
    Serialize a data point for issue checks history graphs
    """

    date = serializers.DateField()
    total = serializers.IntegerField()
