#  software-properties PPA support, using launchpadlib
#
#  Copyright (c) 2019 Canonical Ltd.
#
#  Original Author: Michael Vogt <mvo@debian.org>
#  Rewrite: Dan Streetman <ddstreet@canonical.com>
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License as
#  published by the Free Software Foundation; either version 2 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 General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
#  USA

from gettext import gettext as _

from launchpadlib.launchpad import Launchpad
from lazr.restfulclient.errors import (NotFound, BadRequest, Unauthorized)

from softwareproperties.shortcuthandler import (ShortcutHandler, ShortcutException,
                                                InvalidShortcutException)
from softwareproperties.sourceslist import SourcesListShortcutHandler
from softwareproperties.uri import URIShortcutHandler

from aptsources.sourceslist import Deb822SourceEntry

from urllib.parse import urlparse


PPA_URI_FORMAT = 'https://ppa.launchpadcontent.net/{team}/{ppa}/ubuntu/'
PRIVATE_PPA_URI_FORMAT = 'https://private-ppa.launchpadcontent.net/{team}/{ppa}/ubuntu/'
PPA_VALID_HOSTNAMES = [
    urlparse(PPA_URI_FORMAT).hostname,
    urlparse(PRIVATE_PPA_URI_FORMAT).hostname,
    # Old hostnames.
    'ppa.launchpad.net',
    'private-ppa.launchpad.net',
]

PPA_VALID_COMPS = ['main', 'main/debug']


class PPAShortcutHandler(ShortcutHandler):
    def __init__(self, shortcut, login=False, lp=None, **kwargs):
        super(PPAShortcutHandler, self).__init__(shortcut, deb822=True, **kwargs)
        self._lp_anon = not login
        self._signing_key_data = None

        self._lp = lp                       # LP object
        self._lpteam = None                 # Person/Team LP object
        self._lpppa = None                  # PPA Archive LP object

        self._is_sourceslist = False

        # one of these will set teamname and ppaname, and maybe _source_entry
        if not self._match_ppa(shortcut):
            # The input is either a sources.list line or a URI. Both cases lead
            # to the SourcesListShortcutHandler being used, so unset
            # self.deb822 (LP: #2037210).
            self.deb822 = False

            if not any((self._match_uri(shortcut),
                        self._match_sourceslist(shortcut))):
                msg = (_("ERROR: '%s' is not a valid ppa format") % shortcut)
                raise InvalidShortcutException(msg)

        self._filebase = "%s-ubuntu-%s" % (self.teamname, self.ppaname)
        self._set_auth()

        # Make sure we can find/access the PPA, lp:#1965180
        if self._is_sourceslist:
            try:
                self.lpppa
            except ShortcutException:
                raise InvalidShortcutException(_("ERROR: Can't find ppa"))

        if not self._source_entry:
            comps = self.components
            if not comps:
                comps = ['main']
                if self.lpppa.publish_debug_symbols:
                    print("PPA publishes dbgsym, you may need to include 'main/debug' component")
                    # comps += ['main/debug']

            uri_format = PRIVATE_PPA_URI_FORMAT if self.lpppa.private else PPA_URI_FORMAT
            uri = uri_format.format(team=self.teamname, ppa=self.ppaname)

            entry = Deb822SourceEntry(None, '')
            entry.types = [self.binary_type]
            entry.uris = [uri]
            entry.suites = [self.dist]
            entry.comps = comps

            self._set_source_entry(str(entry))

    @property
    def lp(self):
        if not self._lp:
            if self._lp_anon:
                login_func = Launchpad.login_anonymously
            else:
                login_func = Launchpad.login_with
            self._lp = login_func("%s.%s" % (self.__module__, self.__class__.__name__),
                                  service_root='production',
                                  version='devel')
        return self._lp

    @property
    def lpteam(self):
        if not self._lpteam:
            try:
                self._lpteam = self.lp.people(self.teamname)
            except NotFound:
                msg = (_("ERROR: user/team '%s' not found (use --login if private)") % self.teamname)
                raise ShortcutException(msg)
            except Unauthorized:
                msg = (_("ERROR: invalid user/team name '%s'") % self.teamname)
                raise ShortcutException(msg)
        return self._lpteam

    @property
    def lpppa(self):
        if not self._lpppa:
            try:
                self._lpppa = self.lpteam.getPPAByName(name=self.ppaname)
            except NotFound:
                msg = (_("ERROR: ppa '%s/%s' not found (use --login if private)") %
                       (self.teamname, self.ppaname))
                raise ShortcutException(msg)
            except BadRequest:
                msg = (_("ERROR: invalid ppa name '%s'") % self.ppaname)
                raise ShortcutException(msg)
        return self._lpppa

    @property
    def description(self):
        return self.lpppa.description

    @property
    def web_link(self):
        return self.lpppa.web_link

    @property
    def trustedparts_content(self):
        if not self._signing_key_data:
            key = self.lpppa.getSigningKeyData()
            fingerprint = self.lpppa.signing_key_fingerprint

            if not fingerprint:
                print(_("Warning: could not get PPA signing_key_fingerprint from LP, using anyway"))
            elif 'redacted' in fingerprint:
                print(_("Private PPA fingerprint redacted, using key anyway (LP: #1879781)"))
            elif not fingerprint in self.fingerprints(key):
                msg = (_("Fingerprints do not match, not importing: '%s' != '%s'") %
                       (fingerprint, ','.join(self.fingerprints(key))))
                raise ShortcutException(msg)

            self._signing_key_data = key
        return self._signing_key_data

    def SourceEntry(self, pkgtype=None):
        entry = super(PPAShortcutHandler, self).SourceEntry(pkgtype=pkgtype)
        if pkgtype != self.source_type or self.components:
            return entry

        # 'main/debug' is needed to get dbgsyms from ppas,
        # but it's not a valid component for ppa deb-src lines.
        # Sigh.
        entry.comps = list(set(entry.comps) - set(['main/debug']))
        return entry

    def _set_source_entry(self, line):
        super(PPAShortcutHandler, self)._set_source_entry(line)

        invalid_comps = set(self.SourceEntry().comps) - set(PPA_VALID_COMPS)
        if invalid_comps:
            print(_("Warning: components '%s' not valid for PPA") % ' '.join(invalid_comps))

    def _match_ppa(self, shortcut):
        (prefix, _, ppa) = shortcut.rpartition(':')
        if not prefix.lower() == 'ppa':
            return False

        (teamname, _, ppaname) = ppa.partition('/')
        teamname = teamname.lstrip('~')
        if '/' in ppaname:
            (ubuntu, _, ppaname) = ppaname.partition('/')
            if ubuntu.lower() != 'ubuntu':
                # PPAs only support ubuntu
                return False
            if '/' in ppaname:
                # Path is too long for valid ppa
                return False

        self.teamname = teamname
        self.ppaname = ppaname or 'ppa'
        return True

    def _match_uri(self, shortcut):
        try:
            return self._match_handler(URIShortcutHandler(shortcut))
        except InvalidShortcutException:
            return False

    def _match_sourceslist(self, shortcut):
        try:
            handler = self._match_handler(SourcesListShortcutHandler(shortcut))
        except InvalidShortcutException:
            return False
        self._is_sourceslist = True
        return handler

    def _match_handler(self, handler):
        parsed = urlparse(handler.SourceEntry().uri)
        if not parsed.hostname in PPA_VALID_HOSTNAMES:
            return False

        path = parsed.path.strip().strip('/').split('/')
        if len(path) < 2:
            return False
        self.teamname = path[0]
        self.ppaname = path[1]

        self._username = handler.username
        self._password = handler.password

        self._set_source_entry(handler.SourceEntry().line)
        return True

    def _set_auth(self):
        if self._lp_anon or not self.lpppa.private:
            return

        if self._username and self._password:
            return

        try:
            # Named ops work against actual person, not the me alias object
            me = self.lp.people(self.lp.me.name)
            url = me.getArchiveSubscriptionURL(archive=self.lpppa)
        except Unauthorized:
            msg = (_("Could not find PPA subscription for ppa:%s/%s, you may need to request access") %
                   (self.teamname, self.ppaname))
            raise ShortcutException(msg)
        else:
            parsed = urlparse(url)
            self._username = parsed.username
            self._password = parsed.password
