from abc import ABC, abstractmethod
from enum import IntEnum
from datetime import datetime, time, date, timedelta
import re
import unicodedata
from django.utils import timezone
import logging


class Extractor(ABC):
    class Warning(IntEnum):
        NO_TITLE = 1
        NO_START_DATE = 2
        NOT_FOUND = 3

    url_referer=None

    def __init__(self):
        self.header = {}
        self.events = []
        self.downloader = None
        self.has_2nd_method = False

        # defined it to true in herited classes will
        # avoid the importer to use the downloader on the url
        # (used for extractors that are self-sufficient)
        self.no_downloader = False
        
        # parameters used by the downloader to get the content
        self.referer = ""
        self.data = None
        self.content_type = None
    
    def prepare_2nd_extract(self):
        pass

    def remove_accents(input_str):
        nfkd_form = unicodedata.normalize("NFKD", input_str)
        return "".join([c for c in nfkd_form if not unicodedata.combining(c)])

    def guess_end_day(start_day, start_time, end_time):
        if end_time:
            if end_time > start_time:
                return start_day
            else:
                return start_day + timedelta(days=1)
        else:
            return start_day

    def guess_month(text):
        mths = [
            "jan",
            "fe",
            "mar",
            "av",
            "mai",
            "juin",
            "juill",
            "ao",
            "sep",
            "oct",
            "nov",
            "dec",
        ]
        t = Extractor.remove_accents(text).lower()
        for i, m in enumerate(mths):
            if t.startswith(m):
                return i + 1
        return None

    def parse_french_date(text, default_year=None, default_year_by_proximity=None):
        # format NomJour Numero Mois Année
        m = re.search(
            "[a-zA-ZéÉûÛ:.]+[ ]*([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)[ ]*([0-9]+)", text
        )
        if m:
            day = m.group(1)
            month = Extractor.guess_month(m.group(2))
            year = m.group(3)
        else:
            # format Numero Mois Annee
            m = re.search("([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)[ ]*([0-9]+)", text)
            if m:
                day = m.group(1)
                month = Extractor.guess_month(m.group(2))
                year = m.group(3)
            else:
                # format Numero Mois Annee
                m = re.search("([0-9]+)/([0-9]+)/([0-9]+)", text)
                if m:
                    day = m.group(1)
                    month = int(m.group(2))
                    year = m.group(3)
                else:
                    # format Numero Mois Annee
                    m = re.search("([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)", text)
                    if m:
                        day = m.group(1)
                        month = Extractor.guess_month(m.group(2))
                        year = default_year
                    else:
                        # TODO: consolider les cas non satisfaits
                        return None

        if month is None:
            return None
        try:
            day = int(day)
            if not year is None:
                year = int(year)
        except:
            return None
        if day >= 32:
            return None

        # by proximity
        if year is None and not default_year_by_proximity is None:
            dates = [date(default_year_by_proximity.year + x, month, day) for x in [-1, 0, 1]]
            dates = [(abs((d - default_year_by_proximity).days), d) for d in dates]
            d = min(dates, key=lambda x: x[0])
            return d[1]

        if year is None:
            return None

        if year < 100:
            year = 2000 + year
        return date(year, month, day)

    def parse_french_time(text):
        # format heures minutes secondes
        m = re.search("([0-9]+)[ a-zA-Z:.]+([0-9]+)[ a-zA-Z:.]+([0-9]+)", text)
        if m:
            h = m.group(1)
            m = m.group(2)
            s = m.group(3)
        else:
            # format heures minutes
            m = re.search("([0-9]+)[ hH:.]+([0-9]+)", text)
            if m:
                h = m.group(1)
                m = m.group(2)
                s = "0"
            else:
                # format heures
                m = re.search("([0-9]+)[ ]*[Hh:.]", text)
                if m:
                    h = m.group(1)
                    m = "0"
                    s = "0"
                else:
                    # format minutes
                    m = re.search("([0-9]+)[ ]*(?:mn|min|Min|Mn)", text)
                    if m:
                        h = "0"
                        m = m.group(1)
                        s = "0"
                    else:
                        return None

        try:
            h = int(h)
            m = int(m)
            s = int(s)
        except:
            return None
        if h >= 24 or m >= 60 or s >= 60:
            return None
        return time(h, m, s)

    @abstractmethod
    def extract(
        self, content, url, url_human=None, default_values=None, published=False
    ):
        pass

    def set_downloader(self, downloader):
        self.downloader = downloader

    @abstractmethod
    def clean_url(url):
        return url

    def is_known_url(url):
        return False

    def set_header(self, url):
        self.header["url"] = url
        self.header["date"] = datetime.now()

    def clear_events(self):
        self.events = []

    def add_event(
        self,
        default_values,
        title,
        category,
        start_day,
        location,
        description,
        tags,
        uuids,
        recurrences=None,
        url_human=None,
        start_time=None,
        end_day=None,
        end_time=None,
        last_modified=None,
        published=False,
        image=None,
        image_alt=None,
        not_found=False
    ):
        comments = ''
        warnings = []
        if title is None:
            print("WARNING: cannot publish an event without name")
            published = False
            title = _('Unknown title')
            warnings.append(Extractor.Warning.NO_TITLE)
        if start_day is None:
            print("WARNING: cannot publish an event without start day")
            published = False
            start_day = datetime.now().date().strftime("%Y-%m-%d")
            warnings.append(Extractor.Warning.NO_START_DATE)
        if not_found:
            warnings.append(Extractor.Warning.NOT_FOUND)

        tags_default = self.default_value_if_exists(default_values, "tags")
        if not tags_default:
            tags_default = []

        event = {
            "title": title,
            "category": category if category else self.default_value_if_exists(default_values, "category"),
            "start_day": start_day,
            "uuids": uuids,
            "location": location if location else self.default_value_if_exists(default_values, "location"),
            "organisers": self.default_value_if_exists(default_values, "organisers"),
            "description": description,
            "tags": tags + tags_default,
            "published": published,
            "image": image,
            "image_alt": image_alt,
            "email": self.default_value_if_exists(default_values, "email"),
            "comments": self.default_value_if_exists(default_values, "comments"),
            "warnings": warnings,
        }
        if event["comments"] is None:
            event["comments"] = comments
        else:
            event["comments"] += '\n' + comments

        # TODO: pourquoi url_human et non reference_url
        if url_human is not None:
            event["url_human"] = url_human
        if start_time is not None:
            event["start_time"] = start_time
        if end_day is not None:
            event["end_day"] = end_day
        if end_time is not None:
            event["end_time"] = end_time

        if last_modified is not None:
            event["last_modified"] = last_modified

        if recurrences is not None:
            event["recurrences"] = recurrences

        self.events.append(event)

    def default_value_if_exists(self, default_values, key):
        return (
            default_values[key]
            if default_values is not None and key in default_values
            else None
        )

    def get_structure(self):
        if len(self.events) == 0:
            return {}
        else:
            return {"header": self.header, "events": self.events}

    def clean_url(url):
        from .generic_extractors.ical import ICALExtractor
        from .generic_extractors.fbevent import CExtractor as FacebookEventExtractor

        result = url
        for e in [ICALExtractor, FacebookEventExtractor]:
            result = e.clean_url(result)
        return result

    def get_default_extractors(single_event=False):
        from .generic_extractors.ical import ICALExtractor
        from .generic_extractors.fbevent import CExtractor as FacebookEventExtractor
        from .generic_extractors.ggcal_link import CExtractor as GoogleCalendarLinkEventExtractor

        if single_event:
            return [FacebookEventExtractor(), GoogleCalendarLinkEventExtractor(), EventNotFoundExtractor()]
        else:
            return [ICALExtractor(), FacebookEventExtractor(), GoogleCalendarLinkEventExtractor(), EventNotFoundExtractor()]


# A class that only produce a not found event
class EventNotFoundExtractor(Extractor):

    def extract(
        self, content, url, url_human=None, default_values=None, published=False
    ):
        self.set_header(url)
        self.clear_events()

        self.add_event(default_values, "événement sans titre depuis " + url, 
            None, timezone.now().date(), None, 
            "l'import a échoué, la saisie doit se faire manuellement à partir de l'url source " + url,
            [],  [url], published=False, url_human=url, not_found=True)

        return self.get_structure()


    def clean_url(url):
        return url

