# coding: utf8
# Debian Contributors data source core data structures
#
# Copyright (C) 2013  Enrico Zini <enrico@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
import re

from .parser import Fail

__all__ = ["Identifier", "Contribution"]


class Identifier:
    """
    Information about a user identifier
    """
    __slots__ = ("type", "id", "desc")

    # Validator regexps
    TYPE_VALIDATORS = {
        "login": re.compile(r"^[a-z0-9._-]+$"),
        # From http://www.regular-expressions.info/email.html
        "email": re.compile(r"^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}", re.I),
        "fpr": re.compile(r"^[A-F0-9]{32,40}$"),
        "url": re.compile(r"^https?://"),
        # "wiki": re.compile("^[A-Za-z]+$"),
    }

    def __init__(self, type:str, id: str, desc: str | None = None) -> None:
        self.type = type
        self.id = id
        self.desc = desc

    def __hash__(self):
        return hash(self.type) + hash(self.id)

    def __eq__(self, other):
        return (self.type == other.type
                and self.id == other.id)

    def to_json(self):
        """
        Return a JSON-serializable structure for this identifier
        """
        if self.desc:
            return {
                "type": self.type,
                "id": self.id,
                "desc": self.desc,
            }
        else:
            return {
                "type": self.type,
                "id": self.id,
            }

    def validate(self):
        """
        Validate the contents of this Identifier, raising parser.Fail if
        anything fails.
        """
        # Validate member types
        if not isinstance(self.type, str):
            raise Fail(400, "Identifier type is '{}' instead of a string".format(type(self.type)))
        if not isinstance(self.id, str):
            raise Fail(400, "Identifier id is '{}' instead of a string".format(type(self.id)))
        if self.desc is not None and not isinstance(self.desc, str):
            raise Fail(400, "Identifier desc is '{}' instead of None or a string".format(type(self.desc)))

        # Validator for this type
        type_validator = self.TYPE_VALIDATORS.get(self.type, None)
        if type_validator is None:
            raise Fail(400, "Invalid identifier type '{}'".format(self.type))

        # Parse the ID and validate it
        if not type_validator.match(self.id):
            raise Fail(400, "{} '{}' is not a valid identifier".format(self.type, self.id))

    @classmethod
    def create_auto(cls, s, default_desc=None):
        """
        Autodetect identifier type and value from a string.

        'desc' is the default description to use if not inferred automatically.
        """
        from email.utils import getaddresses
        if "<" in s:
            # Use getaddresses instead of parseaddr because
            # parseaddr truncates the string at a stray command, instead of
            # declaring a failed parse:
            # parseaddr("a, <b>") gives ('', 'a')
            results = getaddresses((s,))
            if len(results) == 1:
                # Parsing was ok
                desc, ident = results[0]
            else:
                # Something went wrong, possibly a stray comma. Trying again
                # wtih a regexp
                mo = re.match(r"^\s*(?:(.+)\s+)?<([^>]+)>\s*$", s)
                if mo:
                    desc, ident = mo.group(1, 2)
                else:
                    desc, ident = default_desc, s
        else:
            desc, ident = default_desc, s
        ident = ident.replace(" ", "")
        for type, regexp in cls.TYPE_VALIDATORS.items():
            if regexp.match(ident):
                return cls(type, ident, desc)
        raise ValueError("cannot infer a valid Identifier from '{}'".format(s))


class Contribution:
    """
    Information about a contribution.
    """
    __slots__ = ("type", "begin", "end", "url")

    def __init__(self, type, begin=None, end=None, url=None):
        """
        type: contribution type (as configured in contrbutors.debian.org for a
              source)
        begin: start time of this contribution. None to reuse the last start time.
        end: end time of this contribution. None to mean 'now'.
        url: URL used to list all contributions of this type from this person,
             if available.
        """
        self.type = type
        self.begin = begin
        self.end = end
        self.url = url

    def __hash__(self):
        return hash(self.type) + hash(self.begin) + hash(self.end)

    def __eq__(self, other):
        return (self.type == other.type
                and self.begin == other.begin
                and self.end == other.end)

    def extend_by_date(self, date):
        """
        Extend the date range to include the given date

        "Extend" is a bit imprecise: if the current end date is None (meaning
        'today'), then it is set to 'date' (which could be before than today)
        """
        if self.begin is None:
            self.begin = date
        else:
            self.begin = min(self.begin, date)

        if self.end is None:
            self.end = date
        else:
            self.end = max(self.end, date)

    def to_json(self):
        """
        Return a JSON-serializable structure for this contribution
        """
        res = {"type": self.type}
        if self.begin:
            res["begin"] = self.begin.strftime("%Y-%m-%d")
        if self.end:
            res["end"] = self.end.strftime("%Y-%m-%d")
        if self.url:
            res["url"] = self.url
        return res

    @classmethod
    def merged(cls, first, second):
        """
        Build a Contribution with a merge of two existing ones
        """
        if second.begin is None:
            begin = first.begin
        elif first.begin is None:
            begin = second.begin
        else:
            begin = min(first.begin, second.begin)

        if second.end is None:
            end = first.end
        elif first.end is None:
            end = second.end
        else:
            end = max(first.end, second.end)

        if first.url is None:
            url = second.url
        else:
            url = first.url

        return cls(first.type, begin, end, url)

    def validate(self):
        """
        Validate the contents of this Identifier, raising parser.Fail if
        anything fails.
        """
        # Validate member types
        if not isinstance(self.type, str):
            raise Fail(400, "Contribution type is '{}' instead of a string".format(type(self.type)))
        if self.begin is not None and not hasattr(self.begin, "strftime"):
            raise Fail(400, "Contribution begin is '{}' and does not look like a date or datetime".format(
                type(self.begin)))
        if self.end is not None and not hasattr(self.end, "strftime"):
            raise Fail(400, "Contribution end is '{}' and does not look like a date or datetime".format(type(self.end)))
        if self.url is not None and not isinstance(self.url, str):
            raise Fail(400, "Contribution URL is '{}' instead of None or a string".format(type(self.url)))
