import json
import logging
from datetime import date, datetime, time

from django.db.models import Q
from django.contrib.auth.models import User
from .import_tasks.extractor import Extractor
from django.core.files.storage import default_storage
from django.utils.translation import gettext_lazy as _
import recurrence
import recurrence.fields

from django.utils import timezone

from agenda_culturel.models import Event, Message, Category, remove_accents

logger = logging.getLogger(__name__)


class DBImporterEvents:
    def __init__(self, celery_id):
        self.celery_id = celery_id
        self.error_message = ""
        self.user_id = None
        self.init_result_properties()
        self.today = timezone.now().date().isoformat()

    def init_result_properties(self):
        self.event_objects = []
        self.db_event_objects = []
        self.nb_updated = 0
        self.nb_removed = 0
        self.date = None
        self.url = None

    def get_nb_events(self):
        return len(self.event_objects)

    def get_nb_imported_events(self):
        return len(self.db_event_objects)

    def get_nb_updated_events(self):
        return self.nb_updated

    def get_nb_removed_events(self):
        return self.nb_removed

    def import_events(self, json_structure, user_id=None):
        self.init_result_properties()
        self.user_id = user_id

        try:
            structure = json.loads(json_structure)
        except Exception as e:
            return (False, "JSON file is not correctly structured: " + str(e))

        if len(structure) == 0:
            return (True, "")

        if "header" not in structure:
            return (False, "JSON is not correctly structured: missing header")
        if "events" not in structure:
            return (False, "JSON is not correctly structured: missing events")

        if "url" in structure["header"]:
            self.url = structure["header"]["url"]
        else:
            return (
                False,
                "JSON is not correctly structured: missing url in header",
            )

        if "date" in structure["header"]:
            self.date = structure["header"]["date"]
        else:
            self.date = str(date.today())

        # get events
        for event in structure["events"]:
            # only process events if they are today or the days after

            if self.event_takes_place_today_or_after(event):
                # set a default "last modified date"
                if "last_modified" not in event and self.date is not None:
                    event["last_modified"] = self.date

                # conversion to Event, and return an error if it failed
                if not self.load_event(event):
                    return (False, self.error_message)
            else:
                logger.warning(
                    "Event in the past, will not be imported: {}".format(event)
                )

        # finally save the loaded events in database
        self.save_imported()

        return (True, "")

    def event_takes_place_today_or_after(self, event):
        # not optimal, but will work: import recurrent events even if they come from the past
        if "recurrences" in event:
            return True

        if "start_day" not in event:
            return False

        if event["start_day"] >= self.today:
            return True

        if "end_day" not in event:
            return False

        return event["end_day"] >= self.today

    def save_imported(self):
        self.db_event_objects, self.nb_updated, self.nb_removed = (
            DBImporterEvents.import_in_db(
                self.event_objects,
                remove_missing_from_source=self.url,
                user_id=self.user_id,
            )
        )

    # Limitation: the given events should not be considered similar one to another...
    def import_in_db(events, remove_missing_from_source=None, user_id=None):
        user = None
        if user_id:
            user = User.objects.filter(pk=user_id).first()

        to_import = []
        to_update = []

        min_date = timezone.now().date()
        max_date = None
        uuids = set()

        # for each event, check if it's a new one, or a one to be updated
        for event in events:
            sdate = date.fromisoformat(event.start_day)
            if event.end_day:
                edate = date.fromisoformat(event.end_day)
            else:
                edate = sdate
            if min_date is None or min_date > sdate:
                min_date = sdate
            if max_date is None or max_date < sdate:
                max_date = sdate
            if max_date is None or (event.end_day is not None and max_date < edate):
                max_date = edate
            if event.uuids and len(event.uuids) > 0:
                uuids |= set(event.uuids)

            # imported events should be updated
            event.set_in_importation_process()
            event.set_processing_user(user)
            event.prepare_save()

            # check if the event has already be imported (using uuid)
            same_events = event.find_same_events_by_uuid()

            if same_events is not None and len(same_events) != 0:
                # check if one event has been imported and not modified in this list
                same_imported = Event.find_last_pure_import(same_events)
                pure = True
                # if not, we check if it does not match exactly with another
                if not same_imported:
                    for e in same_events:
                        if event.similar(e, False):
                            same_imported = e
                            pure = False
                            break

                if same_imported:
                    if not event.similar(same_imported, False):
                        # reopen DuplicatedEvents if required
                        if same_imported.other_versions:
                            if same_imported.status != Event.STATUS.TRASH:
                                if same_imported.other_versions.is_published():
                                    if (
                                        same_imported.other_versions.representative
                                        != same_imported
                                    ):
                                        same_imported.other_versions.representative = (
                                            None
                                        )
                                        same_imported.other_versions.save()
                        # add a message to explain the update
                        if not event.is_not_found_import():
                            res = [
                                r
                                for r in Event.get_comparison(
                                    [event, same_imported], all
                                )
                                if not r["similar"]
                            ]
                            if len(res) > 0:
                                txt = _("Updated field(s): ") + ", ".join(
                                    [r["key"] for r in res]
                                )
                                msg = Message(
                                    subject=_("Update"),
                                    name=_("update process"),
                                    related_event=same_imported,
                                    message=txt,
                                    message_type=Message.TYPE.UPDATE_PROCESS,
                                )
                                msg.save()

                    new_image = same_imported.image != event.image

                    # if the import process was not able to found any content, change the status as draft
                    if event.is_not_found_import():
                        if same_imported.status == Event.STATUS.PUBLISHED:
                            same_imported.status = Event.STATUS.TRASH
                    else:
                        # we only update local information if it's a pure import and has no moderated_date
                        same_imported.update(
                            event,
                            pure and same_imported.moderated_date is None,
                        )
                    # save messages
                    if event.has_message():
                        for msg in event.get_messages():
                            msg.related_event = same_imported
                            msg.save()
                    same_imported.set_in_importation_process()
                    same_imported.prepare_save()
                    # fix missing or updated files
                    if same_imported.local_image and (
                        not default_storage.exists(same_imported.local_image.name)
                        or new_image
                    ):
                        same_imported.download_image()
                        same_imported.save(
                            update_fields=["local_image"],
                            noclean_other_versions=True,
                        )

                    to_update.append(same_imported)
                else:
                    # otherwise, the new event possibly a duplication of the remaining others.

                    # check if it should be published
                    trash = (
                        len([e for e in same_events if e.status != Event.STATUS.TRASH])
                        == 0
                    )
                    if trash:
                        event.status = Event.STATUS.TRASH
                    event.set_other_versions(same_events, force_non_fixed=not trash)
                    # it will be imported
                    to_import.append(event)
            else:
                # if uuid is unique (or not available), check for similar events
                similar_events = event.find_similar_events()

                # if it exists similar events, add this relation to the event
                if len(similar_events) != 0:
                    # the event is possibly a duplication of the others
                    event.set_other_versions(similar_events, force_non_fixed=True)
                    to_import.append(event)
                else:
                    # import this new event
                    to_import.append(event)

        for e in to_import:
            if e.is_event_long_duration():
                e.status = Event.STATUS.DRAFT
                e.add_message(
                    Message(
                        subject=_("Import"),
                        name=_("import process"),
                        message=_(
                            "The duration of the event is a little too long for direct publication. Moderators can choose to publish it or not."
                        ),
                        message_type=Message.TYPE.IMPORT_PROCESS,
                    )
                )

        # then import all the new events
        imported = Event.objects.bulk_create(to_import)
        # update organisers (m2m relation)
        for i, ti in zip(imported, to_import):
            if ti.has_pending_organisers() and ti.pending_organisers is not None:
                i.organisers.set(ti.pending_organisers)
            if ti.has_message():
                for msg in ti.get_messages():
                    msg.related_event = i
                    msg.save()

        nb_updated = Event.objects.bulk_update(
            to_update,
            fields=Event.data_fields(no_m2m=True)
            + [
                "imported_date",
                "modified_date",
                "uuids",
                "status",
                "import_sources",
            ],
        )

        nb_draft = 0
        if remove_missing_from_source is not None and max_date is not None:
            # events that are missing from the import but in database are turned into drafts
            # only if they are in the future

            in_interval = Event.objects.filter(
                (
                    (
                        Q(end_day__isnull=True)
                        & Q(start_day__gte=min_date)
                        & Q(start_day__lte=max_date)
                    )
                    | (
                        Q(end_day__isnull=False)
                        & ~(Q(start_day__gt=max_date) | Q(end_day__lt=min_date))
                    )
                )
                & Q(import_sources__contains=[remove_missing_from_source])
                & Q(status=Event.STATUS.PUBLISHED)
                & Q(uuids__len__gt=0)
            )

            to_draft = []
            for e in in_interval:
                if len(uuids.intersection(e.uuids)) == 0:
                    e.status = Event.STATUS.TRASH
                    # save them without updating modified date
                    e.set_no_modification_date_changed()
                    e.prepare_save()
                    to_draft.append(e)

            nb_draft = Event.objects.bulk_update(to_draft, fields=["status"])

        return imported, nb_updated, nb_draft

    def is_valid_event_structure(self, event):
        if "title" not in event:
            self.error_message = (
                "JSON is not correctly structured: one event without title"
            )
            return False
        if "start_day" not in event:
            self.error_message = (
                "JSON is not correctly structured: one event without start_day"
            )
            return False
        return True

    def load_event(self, event):
        if self.is_valid_event_structure(event):
            logger.info(
                "Valid event: {} {}".format(event["last_modified"], event["title"])
            )
            event_obj = DBImporterEvents.from_structure(event, self.url)
            self.event_objects.append(event_obj)
            return True
        else:
            logger.warning("Not valid event: {}".format(event))
            return False

    def from_structure(event_structure, import_source=None):
        # organisers is a manytomany relation thus cannot be initialised before creation of the event
        organisers = event_structure.pop("organisers", None)
        # supplementary information
        email = event_structure.pop("email", None)
        comments = event_structure.pop("comments", None)
        warnings = event_structure.pop("warnings", [])

        for w in warnings:
            if w == Extractor.Warning.NO_START_DATE:
                event_structure["title"] += (
                    " - "
                    + _("Warning")
                    + ": "
                    + _("the date has not been imported correctly.")
                )
            if w == Extractor.Warning.NO_START_TIME:
                event_structure["title"] += (
                    " - "
                    + _("Warning")
                    + ": "
                    + _("the time has not been imported correctly.")
                )

        if "category" in event_structure and event_structure["category"] is not None:
            try:
                event_structure["category"] = Category.objects.get(
                    name__unaccent__icontains=remove_accents(
                        event_structure["category"].lower()
                    )
                )
            except Category.DoesNotExist:
                event_structure["category"] = Category.get_default_category()
        else:
            event_structure["category"] = Category.get_default_category()

        if "published" in event_structure and event_structure["published"] is not None:
            if event_structure["published"]:
                event_structure["status"] = Event.STATUS.PUBLISHED
            else:
                event_structure["status"] = Event.STATUS.DRAFT
            del event_structure["published"]
        else:
            event_structure["status"] = Event.STATUS.DRAFT

        if "url_human" in event_structure and event_structure["url_human"] is not None:
            event_structure["reference_urls"] = [event_structure["url_human"]]
            del event_structure["url_human"]

        if (
            "last_modified" in event_structure
            and event_structure["last_modified"] is not None
        ):
            d = datetime.fromisoformat(event_structure["last_modified"])
            if d.tzinfo is None or d.tzinfo.utcoffset(d) is None:
                d = timezone.make_aware(d, timezone.get_default_timezone())
            event_structure["modified_date"] = d
            del event_structure["last_modified"]
        else:
            event_structure["modified_date"] = None

        if "start_time" in event_structure and event_structure["start_time"] != "":
            event_structure["start_time"] = time.fromisoformat(
                event_structure["start_time"]
            )
        else:
            event_structure["start_time"] = None

        if "end_time" in event_structure and event_structure["end_time"] != "":
            event_structure["end_time"] = time.fromisoformat(
                event_structure["end_time"]
            )
        else:
            event_structure["end_time"] = None

        if "location" not in event_structure or event_structure["location"] is None:
            event_structure["location"] = ""

        if "description" in event_structure and event_structure["description"] is None:
            event_structure["description"] = ""

        if (
            "recurrences" in event_structure
            and event_structure["recurrences"] is not None
        ):
            event_structure["recurrences"] = recurrence.deserialize(
                event_structure["recurrences"]
            )
            event_structure["recurrences"].exdates = [
                e.replace(hour=0, minute=0, second=0)
                for e in event_structure["recurrences"].exdates
            ]
            event_structure["recurrences"].rdates = [
                e.replace(hour=0, minute=0, second=0)
                for e in event_structure["recurrences"].rdates
            ]

        else:
            event_structure["recurrences"] = None

        if import_source is not None:
            event_structure["import_sources"] = [import_source]

        result = Event(**event_structure)
        result.add_pending_organisers(organisers)
        if email or comments:
            has_comments = comments not in ["", None]
            result.add_message(
                Message(
                    subject=_("during import process"),
                    email=email,
                    message=comments,
                    closed=False,
                    message_type=(
                        Message.TYPE.FROM_CONTRIBUTOR
                        if has_comments
                        else Message.TYPE.FROM_CONTRIBUTOR_NO_MSG
                    ),
                )
            )
        for w in warnings:
            if w == Extractor.Warning.NO_START_DATE:
                result.set_invalid_start_date()
                result.add_message(
                    Message(
                        subject=_("warning"),
                        closed=False,
                        message=_("the date has not been imported correctly."),
                        message_type=Message.TYPE.WARNING,
                    )
                )
            if w == Extractor.Warning.NO_START_TIME:
                result.set_invalid_start_time()
                result.add_message(
                    Message(
                        subject=_("warning"),
                        closed=False,
                        message=_("the time has not been imported correctly."),
                        message_type=Message.TYPE.WARNING,
                    )
                )
            if w == Extractor.Warning.NO_TITLE:
                result.set_invalid_title()
                result.add_message(
                    Message(
                        subject=_("warning"),
                        closed=False,
                        message=_("the title has not been imported correctly."),
                        message_type=Message.TYPE.WARNING,
                    )
                )
            if w == Extractor.Warning.NOT_FOUND:
                result.status = Event.STATUS.DRAFT
                result.set_is_not_found_import()
                result.add_message(
                    Message(
                        subject=_("warning"),
                        closed=False,
                        message=_(
                            "The import was unable to find an event in the page."
                        ),
                        message_type=Message.TYPE.WARNING,
                    )
                )

        return result
