#!/usr/bin/python

from __future__ import print_function

import os
from pathlib import Path
from tempfile import TemporaryDirectory
from urllib.request import pathname2url

import osc.conf as conf
import osc.core
from osc.oscerr import OscIOError, WrongArgs

from debian.deb822 import Changes, Dsc


statfrmt = osc.core.statfrmt


def get_objects_from_file(filename):
    """
    Return a tuple containing (None or a Changes object), a Dsc object,
    and the filename of the .dsc file.
    """
    changes = None
    dsc = None
    dscfile = None

    try:
        with open(filename) as f:
            if filename.endswith('.changes'):
                changes = Changes(f)

                if 'source' not in changes['architecture'].split():
                    raise WrongArgs(filename + ' does not contain source code')

                for rec in changes['files']:
                    if rec['name'].endswith('.dsc'):
                        dscfile = Path(filename).with_name(rec['name'])
                        with open(dscfile) as fdsc:
                            dsc = Dsc(fdsc)
                        return changes, dsc, dscfile
                else:
                    raise WrongArgs(filename + ' does not list a .dsc file')
            elif filename.endswith('.dsc'):
                dsc = Dsc(f)
                dscfile = filename
                return changes, dsc, dscfile
            else:
                raise WrongArgs(filename + ' is not a .dsc or .changes file')
    except FileNotFoundError as e:
        raise OscIOError(e, "File " + filename + " not found.")
    except IOError as e:
        raise OscIOError(e, "File " + filename + " couldn't be read.")


class DPut(object):
    def __init__(self, project_name=None, package_name=None, working_dir=None):
        self.project_name = project_name
        self.package_name = package_name
        self.working_dir = Path(working_dir)

    def _create_package(self):
        """
           Create a package on the remote server
        """

        current_path = os.getcwd()
        project_path = self.working_dir / self.project_name

        os.chdir(project_path)
        osc.core.createPackageDir(self.package_name)

        project = osc.core.Project(".", getPackageList=False)
        project.update()
        project.commit()
        os.chdir(current_path)

    def get_package_object(self):
        """
            Returns a valid package object depending if the package already exists or
            if it needs to be created
        """
        # this one is needed because there's two ways of getting the package object
        # first one fails if the folder already exists, the second one fail if it doesn't

        path = self.working_dir / self.project_name / self.package_name
        if path.is_dir():
            os.chdir(path)
            return osc.core.Package('.')

        elif not path.exists():
            path.mkdir()
            os.chdir(path)
            package = osc.core.Package.init_package(conf.config['apiurl'],
                                                    project=self.project_name,
                                                    package=self.package_name,
                                                    dir='.',
                                                    meta=False)
            # Updating the package files with size_limit set to 1 after
            # the initialisation allows us to start with a mostly
            # clean working directory and still proceed without downloading
            # all of the current contents only to possibly remove it in the
            # first step; if we were to configure this in init_package,
            # the setting would get stored in the local checkout config.
            # We override the checkout formatter temporarily to prevent it
            # from printing a scary file listing with all files deleted
            # (which it prints because when files are skipped, it doesn’t
            # find them in a local checkout).
            print("Existing files in the package:")
            osc.core.statfrmt = lambda _, file: statfrmt(' ', file)
            package.update(size_limit=1)
            osc.core.statfrmt = statfrmt

            package.rev = package.latest_rev()
            return package

        else:
            raise IOError("Package folder couldn't be created")

    def get_remote_file_list(self, package):
        """
            Returns a list of files inside an specific package
        """
        return {
                f.name: f for f in osc.core.meta_get_filelist(conf.config['apiurl'], self.project_name, self.package_name, verbose=True)
        }


def do_dput(self, subcmd, opts, *args):

    if len(args) < 1:
        raise WrongArgs('Missing the project name and the source package.\n\n'
                  + self.get_cmd_help('dput'))

    if len(args) == 1:
        if opts.no_auto:
            raise WrongArgs('Missing the project name.\n\n'
                  + self.get_cmd_help('dput'))
        dsc_or_changes_file = args[0]
        cwd = Path.cwd()
        if osc.core.is_project_dir(cwd) or osc.core.is_package_dir(cwd):
            proj_name = osc.core.store_read_project(cwd)
        elif osc.core.is_project_dir(cwd.parent) or osc.core.is_package_dir(cwd.parent):
            proj_name = osc.core.store_read_project(cwd.parent)
        else:
            raise WrongArgs('Can\'t guess the project name and none passed.')
        print("Automatically detected the project to submit changes to:", proj_name)
    elif len(args) == 2:
        proj_name, dsc_or_changes_file = args
    else:
        raise WrongArgs('Too many arguments, not sure what to do.\n\n'
                  + self.get_cmd_help('dput'))

    with TemporaryDirectory('_oscdput') as working_dir:
        # get debian .change object before moving current path to the
        # working_dir
        changes, dsc, dsc_file = get_objects_from_file(dsc_or_changes_file)
        package_name = dsc.get("Source")
        dput = DPut(proj_name, package_name, working_dir)

        # Filenames in the .dsc are relative to the directory where it appears.
        # We need to make it absolute before we chdir elsewhere.
        dsc_or_changes_file = Path(dsc_or_changes_file).resolve()
        dsc_file = Path(dsc_file).resolve()
        dscdir = Path(dsc_file).resolve().parent

        # Get the list of packages
        # TODO: Probably it can be done after checking out the project
        # So we can save one http request
        package_list = osc.core.meta_get_packagelist(
            conf.config['apiurl'], proj_name)

        # Start the project
        osc.core.Project.init_project(conf.config['apiurl'],
                                      dir=dput.working_dir / proj_name,
                                      project=proj_name)

        # check if the package exists on server, otherwise create one
        if package_name not in package_list:
            dput._create_package()

        # it also changes the current_dir to the package dir
        package = dput.get_package_object()

        # defining file list, so we can decide which one to delete
        remote_file_list = dput.get_remote_file_list(package)
        local_file_list = [f["name"]
                           for f in dsc.get("Files")]  # local lambda list

        # Remove old files, but only those that are part of the Debian package
        superseded = set()
        retained = set()
        downloaded = set()

        for f in remote_file_list.keys():
            if f.endswith('.dsc'):
                u = osc.core.makeurl(conf.config['apiurl'],
                                     ['source',
                                      proj_name,
                                      package_name,
                                      pathname2url(f)],
                                     query={})

                remote_dsc = Dsc(osc.core.streamfile(u, bufsize='line'))

                for entry in remote_dsc.get('Files'):
                    superseded.add(entry['name'])

                superseded.add(f)
            elif f.endswith('.changes'):
                superseded.add(f)
            else:
                retained.add(f)

        retained -= superseded
        # It may happen that there were some files lying around which
        # we still want to overwrite
        retained -= set(local_file_list)

        # reset the file list, so that we can add files freely without
        # osc barking at us each time we re-add an existing file
        package.filenamelist = []

        # adding local_file_list to the package as links
        for f in local_file_list:
            filepath = dscdir / f
            os.symlink(filepath, f)
            package.addfile(f)

        f = dsc_file.name
        os.symlink(dsc_file, f)
        package.addfile(f)

        if opts.maintained_in_git and 'MAINTAINED_IN_GIT.txt' not in retained:
            open('MAINTAINED_IN_GIT.txt', 'w+').close()
            package.addfile('MAINTAINED_IN_GIT.txt')

        # a plain .dsc file has very little metadata, so "hello_2.10-1.dsc"
        # is about the best commit message we can provide without unpacking
        # the source package
        msg = f

        for f in downloaded:
            package.addfile(f)

        if changes is not None:
            f = dsc_or_changes_file.name
            os.symlink(dsc_or_changes_file, f)
            package.addfile(f)

            msg = changes.get('changes', msg)

        added = set(package.to_be_added)
        # We don’t want to create commits with no actual changes.
        # We only proceed with the commit in these cases:
        # 1. If any new file is added
        # 2. Any of the files is of a different size than recorded
        # 3. Any of the files has a different MD5 hash than recorded
        files_changed = (any(added - superseded - retained) or
                         any(os.path.getsize(f) != remote_file_list[f].size for f in superseded & added) or
                         any(osc.core.dgst(f) != remote_file_list[f].md5 for f in superseded & added))

        if files_changed:
            package.commit(msg=msg)
        else:
            print("No changes, not submitting anything")
