#!/usr/bin/python

from __future__ import print_function

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

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

from debian.deb822 import Changes, Dsc


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=True)
            package.update()
            # Now, this is a hack.
            # OBS tracks metadata and package data separately. Separate
            # revlogs and separate trees. When we create a checkout with
            # meta=True, osc will only request metadata part of the package.
            # This is good for us, since it 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.
            # However, this also means that we cannot request contents of the
            # latest package tree if we want to retain some files.
            # osc used to allow downloading files for package checkouts with
            # meta=True, but it no longer does.
            # An alternative hack is to create a checkout with meta=False,
            # set the max. download size to 1 byte, check out the latest
            # revision and mark all existing files for deletion.
            # Or we can flip the meta flag back to False after we’re done
            # and manually rewind the current revision to the latest one.
            # By the way, turns out _meta and _link aren’t needed after all.
            package.meta = False
            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)

        # The temporary checkout we're using doesn't seem to have the
        # expected result for latest_rev()?
        source_rev = osc.core.get_source_rev(conf.config['apiurl'],
                                             proj_name, package_name)

        if 'rev' in source_rev:
            latest_revision = source_rev['rev']

            for f in retained:
                print('retaining non-source file: %r' % f)
                if not os.path.exists(f):
                    print('fetching %r from server' % f)

                    try:
                        package.updatefile(f, latest_revision)
                    except HTTPError as e:
                        # _link sometimes appears in remote_file_list but can't
                        # be downloaded
                        if f == '_link' and e.code == 404:
                            print('Ignoring 404 error for _link')
                        else:
                            raise
                    else:
                        downloaded.add(f)

        # 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")
