# coding: utf8
# Debian Contributors data source core data structure
#
# Copyright (C) 2013--2014  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/>.
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from collections import defaultdict
from .types import *
import sys
import json

__all__ = ["Submission"]

DEFAULT_BASE_URL = "https://contributors.debian.org/"

class Submission(object):
    """
    A submission to contributors.debian.org
    """
    def __init__(self, name, auth_token=None, baseurl=DEFAULT_BASE_URL):
        """
        name: data source name
        """
        # Data source name, as in the website
        self.name = name
        # Authentication token
        self.auth_token = None
        # Base URL
        self.baseurl = baseurl
        # List of contributions by identifier
        self.entries = defaultdict(dict)
        if auth_token is not None:
            self.set_auth_token(auth_token)

    def set_auth_token(self, auth_token):
        """
        Set the auth token for this source. If auth_token starts with '@', the
        rest is treated as a pathname to a file that contains the token.
        """
        if not auth_token:
            raise ValueError("auth_token is empty")
        if auth_token[0] == "@":
            with open(auth_token[1:], "r") as fd:
                self.auth_token = fd.read().strip()
        else:
            self.auth_token = auth_token

    def add_contribution(self, identifier, contrib):
        """
        Add information about a contribution.

        identifier: Identifier for the user that made this contribution
        contrib: Contribution object
        """
        entries = self.entries[identifier]
        old = entries.get(contrib.type, None)
        if old is None:
            entries[contrib.type] = contrib
        else:
            entries[contrib.type] = Contribution.merged(old, contrib)

    def add_contribution_data(self, identifier, type, begin=None, end=None, url=None):
        """
        Add information about a contribution.

        identifier: Identifier for the user that made this contribution
        name: contribution name (chosen among the source contribution types)
        begin: start time of this contribution. None to reuse the last start time.
        end: end time of this contribution. None to mean 'now'.
        """
        self.add_contribution(identifier, Contribution(type, begin, end, url))

    def merge_with(self, submission):
        """
        Merge another submission into this one
        """
        if self.name != submission.name:
            raise ValueError("Merging submission for two different sources: {}!={}".format(
                self.name, submission.name))
        for ident, contribs in submission.entries.iteritems():
            old = self.entries.get(ident, None)
            if old is None:
                self.entries[ident] = dict(contribs)
            else:
                self.entries[ident] = merge_contrib_dicts(old, contribs)

    def _gen_records(self):
        """
        Generate DC records for serialization
        """
        for ident, contributions in self.entries.iteritems():
            yield {
                "id": (ident.to_json(),),
                "contributions": [ c.to_json() for c in contributions.itervalues() ],
            }

    def to_json(self, file=None, indent=None):
        """
        Convert to JSON.

        file: if set to a file-like object, send data there. Else, return the
              JSON data as a string
        indent: passed as-is to the indent parameter of the encoder
        """
        if file is not None:
            return json.dump(list(self._gen_records()), file, indent=indent)
        else:
            return json.dumps(list(self._gen_records()), indent=indent)

    def print_compact(self, file=sys.stdout):
        """
        Make a compact dump of this source to the given file
        """
        for ident, contributions in self.entries.iteritems():
            for ctype, c in sorted(contributions.iteritems()):
                if ident.desc:
                    lead = "{}:{} <{}>".format(ident.type, ident.desc, ident.id)
                else:
                    lead = "{}:{}".format(ident.type, ident.id)

                print("{}: {} from {} to {}".format(lead, c.type, c.begin, c.end), file=file)
                if c.url:
                    print("{}: {} url: {}".format(lead, c.type, c.url), file=file)

    def post(self):
        """
        POST this submission to the contributors server

        Returns a couple (success, info).

        success: a bool, true if everything was imported correctly, false if
                 there has been some problem.
        info:    a dict with detailed status and error information, plus import
                 statistics
        """
        # Yuck! Python's stdlib cannot do file uploads :'(
        # We need to introduce an external dependency for it
        import requests
        import urlparse
        from cStringIO import StringIO

        # Build the POST request to contributors.debian.org
        url = urlparse.urljoin(self.baseurl, '/contributors/post')
        #print("Posting to '{}'...".format(url))

        # Prepare the file to post
        try:
            import lzma
            compress_type = "xz"
            compress = lzma.compress
        except ImportError:
            import gzip
            compress_type = "gzip"
            def compress(data):
                out = StringIO()
                with gzip.GzipFile(mode="wb", fileobj=out) as fd:
                    fd.write(data)
                return out.getvalue()

        file_data = StringIO(compress(self.to_json()))
        files = {
            "data": file_data
        }

        # POST data
        data = {
            "source": self.name,
            "auth_token": self.auth_token,
            "data_compression": compress_type,
        }

        # POST everything to the server
        try:
            res = requests.post(url, data=data, files=files)
        except requests.ConnectionError, e:
            return False, {
                "code": None,
                "errors": [
                    "Connection error: " + unicode(e)
                ]
            }
        except requests.HTTPError, e:
            return False, {
                "code": None,
                "errors": [
                    "Server's HTTP response was malformed: " + unicode(e)
                ]
            }

        # Whether the POST was successful or not, the response body contains
        # information and statistics in JSON format.
        response = res.json()
        if res.status_code == requests.codes.ok:
            return True, response
        else:
            return False, response

    @classmethod
    def from_json(cls, name, data):
        """
        Build a Submission from previously generated JSON

        name: the data source name
        data: the JSON data, either in a string, in a file, or as a parsed data
              structure
        """
        if isinstance(data, basestring):
            data = json.loads(data)
        elif hasattr(data, "read"):
            data = json.load(data)

        res = cls(name)

        from .parser import Parser
        parser = Parser()
        for ids, contribs in parser.parse_submission(data):
            for i in ids:
                res.entries[i] = { c.type: c for c in contribs }

        return res

def merge_contrib_dicts(d1, d2):
    """
    Merge two dicts of contributions from the same identifier.

    Contribution types that happen in both lists will have their timespans
    merged
    """
    res = {}

    # Add elements from d1, merging them with d2 if they also exist in d2
    for ctype, c1 in d1.iteritems():
        c2 = d2.get(ctype, None)
        if c2 is None:
            res[ctype] = c1
        else:
            res[ctype] = Contribution.merged(c1, c2)

    # Add the elements that only exist in d2
    for ctype, c2 in d2.iteritems():
        res.setdefault(ctype, c2)

    return res
