#!/usr/bin/env python
# ***** BEGIN LICENSE BLOCK *****
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
# ***** END LICENSE BLOCK *****
"""mobile_partner_repack.py

"""

from copy import deepcopy
import os
import sys

# load modules from parent dir
sys.path.insert(1, os.path.dirname(sys.path[0]))

from mozharness.base.errors import ZipErrorList
from mozharness.base.log import FATAL
from mozharness.base.transfer import TransferMixin
from mozharness.base.vcs.vcsbase import MercurialScript
from mozharness.mozilla.l10n.locales import LocalesMixin
from mozharness.mozilla.release import ReleaseMixin
from mozharness.mozilla.signing import MobileSigningMixin

SUPPORTED_PLATFORMS = ["android"]


# MobilePartnerRepack {{{1
class MobilePartnerRepack(LocalesMixin, ReleaseMixin, MobileSigningMixin,
                          TransferMixin, MercurialScript):
    config_options = [[
        ['--locale', ],
        {"action": "extend",
         "dest": "locales",
         "type": "string",
         "help": "Specify the locale(s) to repack"
         }
    ], [
        ['--partner', ],
        {"action": "extend",
         "dest": "partners",
         "type": "string",
         "help": "Specify the partner(s) to repack"
         }
    ], [
        ['--locales-file', ],
        {"action": "store",
         "dest": "locales_file",
         "type": "string",
         "help": "Specify a json file to determine which locales to repack"
         }
    ], [
        ['--tag-override', ],
        {"action": "store",
         "dest": "tag_override",
         "type": "string",
         "help": "Override the tags set for all repos"
         }
    ], [
        ['--platform', ],
        {"action": "extend",
         "dest": "platforms",
         "type": "choice",
         "choices": SUPPORTED_PLATFORMS,
         "help": "Specify the platform(s) to repack"
         }
    ], [
        ['--user-repo-override', ],
        {"action": "store",
         "dest": "user_repo_override",
         "type": "string",
         "help": "Override the user repo path for all repos"
         }
    ], [
        ['--release-config-file', ],
        {"action": "store",
         "dest": "release_config_file",
         "type": "string",
         "help": "Specify the release config file to use"
         }
    ], [
        ['--version', ],
        {"action": "store",
         "dest": "version",
         "type": "string",
         "help": "Specify the current version"
         }
    ], [
        ['--buildnum', ],
        {"action": "store",
         "dest": "buildnum",
         "type": "int",
         "default": 1,
         "metavar": "INT",
         "help": "Specify the current release build num (e.g. build1, build2)"
         }
    ]]

    def __init__(self, require_config_file=True):
        self.release_config = {}
        LocalesMixin.__init__(self)
        MercurialScript.__init__(
            self,
            config_options=self.config_options,
            all_actions=[
                "passphrase",
                "clobber",
                "pull",
                "download",
                "repack",
                "upload-unsigned-bits",
                "sign",
                "upload-signed-bits",
                "summary",
            ],
            require_config_file=require_config_file
        )

    # Helper methods {{{2
    def add_failure(self, platform, locale, **kwargs):
        s = "%s:%s" % (platform, locale)
        if 'message' in kwargs:
            kwargs['message'] = kwargs['message'] % {'platform': platform, 'locale': locale}
        super(MobilePartnerRepack, self).add_failure(s, **kwargs)

    def query_failure(self, platform, locale):
        s = "%s:%s" % (platform, locale)
        return super(MobilePartnerRepack, self).query_failure(s)

    # Actions {{{2

    def pull(self):
        c = self.config
        dirs = self.query_abs_dirs()
        repos = []
        replace_dict = {}
        if c.get("user_repo_override"):
            replace_dict['user_repo_override'] = c['user_repo_override']
            # deepcopy() needed because of self.config lock bug :(
            for repo_dict in deepcopy(c['repos']):
                repo_dict['repo'] = repo_dict['repo'] % replace_dict
                repos.append(repo_dict)
        else:
            repos = c['repos']
        self.vcs_checkout_repos(repos, parent_dir=dirs['abs_work_dir'],
                                tag_override=c.get('tag_override'))

    def download(self):
        c = self.config
        rc = self.query_release_config()
        dirs = self.query_abs_dirs()
        locales = self.query_locales()
        replace_dict = {
            'buildnum': rc['buildnum'],
            'version': rc['version'],
        }
        success_count = total_count = 0
        for platform in c['platforms']:
            base_installer_name = c['installer_base_names'][platform]
            base_url = c['download_base_url'] + '/' + \
                c['download_unsigned_base_subdir'] + '/' + \
                base_installer_name
            replace_dict['platform'] = platform
            for locale in locales:
                replace_dict['locale'] = locale
                url = base_url % replace_dict
                installer_name = base_installer_name % replace_dict
                parent_dir = '%s/original/%s/%s' % (dirs['abs_work_dir'],
                                                    platform, locale)
                file_path = '%s/%s' % (parent_dir, installer_name)
                self.mkdir_p(parent_dir)
                total_count += 1
                if not self.download_file(url, file_path):
                    self.add_failure(platform, locale,
                                     message="Unable to download %(platform)s:%(locale)s installer!")
                else:
                    success_count += 1
        self.summarize_success_count(success_count, total_count,
                                     message="Downloaded %d of %d installers successfully.")

    def _repack_apk(self, partner, orig_path, repack_path):
        """ Repack the apk with a partner update channel.
        Returns True for success, None for failure
        """
        dirs = self.query_abs_dirs()
        zip_bin = self.query_exe("zip")
        unzip_bin = self.query_exe("unzip")
        file_name = os.path.basename(orig_path)
        tmp_dir = os.path.join(dirs['abs_work_dir'], 'tmp')
        tmp_file = os.path.join(tmp_dir, file_name)
        tmp_prefs_dir = os.path.join(tmp_dir, 'defaults', 'pref')
        # Error checking for each step.
        # Ignoring the mkdir_p()s since the subsequent copyfile()s will
        # error out if unsuccessful.
        if self.rmtree(tmp_dir):
            return
        self.mkdir_p(tmp_prefs_dir)
        if self.copyfile(orig_path, tmp_file):
            return
        if self.write_to_file(os.path.join(tmp_prefs_dir, 'partner.js'),
                              'pref("app.partner.%s", "%s");' % (partner, partner)
                              ) is None:
            return
        if self.run_command([unzip_bin, '-q', file_name, 'omni.ja'],
                            error_list=ZipErrorList,
                            return_type='num_errors',
                            cwd=tmp_dir):
            self.error("Can't extract omni.ja from %s!" % file_name)
            return
        if self.run_command([zip_bin, '-9r', 'omni.ja',
                             'defaults/pref/partner.js'],
                            error_list=ZipErrorList,
                            return_type='num_errors',
                            cwd=tmp_dir):
            self.error("Can't add partner.js to omni.ja!")
            return
        if self.run_command([zip_bin, '-9r', file_name, 'omni.ja'],
                            error_list=ZipErrorList,
                            return_type='num_errors',
                            cwd=tmp_dir):
            self.error("Can't re-add omni.ja to %s!" % file_name)
            return
        if self.unsign_apk(tmp_file):
            return
        repack_dir = os.path.dirname(repack_path)
        self.mkdir_p(repack_dir)
        if self.copyfile(tmp_file, repack_path):
            return
        return True

    def repack(self):
        c = self.config
        rc = self.query_release_config()
        dirs = self.query_abs_dirs()
        locales = self.query_locales()
        success_count = total_count = 0
        for platform in c['platforms']:
            for locale in locales:
                installer_name = c['installer_base_names'][platform] % {'version': rc['version'], 'locale': locale}
                if self.query_failure(platform, locale):
                    self.warning("%s:%s had previous issues; skipping!" % (platform, locale))
                    continue
                original_path = '%s/original/%s/%s/%s' % (dirs['abs_work_dir'], platform, locale, installer_name)
                for partner in c['partner_config'].keys():
                    repack_path = '%s/unsigned/partner-repacks/%s/%s/%s/%s' % (dirs['abs_work_dir'], partner, platform, locale, installer_name)
                    total_count += 1
                    if self._repack_apk(partner, original_path, repack_path):
                        success_count += 1
                    else:
                        self.add_failure(platform, locale,
                                         message="Unable to repack %(platform)s:%(locale)s installer!")
        self.summarize_success_count(success_count, total_count,
                                     message="Repacked %d of %d installers successfully.")

    def _upload(self, dir_name="unsigned/partner-repacks"):
        c = self.config
        dirs = self.query_abs_dirs()
        local_path = os.path.join(dirs['abs_work_dir'], dir_name)
        rc = self.query_release_config()
        replace_dict = {
            'buildnum': rc['buildnum'],
            'version': rc['version'],
        }
        remote_path = '%s/%s' % (c['ftp_upload_base_dir'] % replace_dict, dir_name)
        if self.rsync_upload_directory(local_path, c['ftp_ssh_key'],
                                       c['ftp_user'], c['ftp_server'],
                                       remote_path):
            self.return_code += 1

    def upload_unsigned_bits(self):
        self._upload()

    # passphrase() in AndroidSigningMixin
    # verify_passphrases() in AndroidSigningMixin

    def preflight_sign(self):
        if 'passphrase' not in self.actions:
            self.passphrase()
            self.verify_passphrases()

    def sign(self):
        c = self.config
        rc = self.query_release_config()
        dirs = self.query_abs_dirs()
        locales = self.query_locales()
        success_count = total_count = 0
        for platform in c['platforms']:
            for locale in locales:
                installer_name = c['installer_base_names'][platform] % {'version': rc['version'], 'locale': locale}
                if self.query_failure(platform, locale):
                    self.warning("%s:%s had previous issues; skipping!" % (platform, locale))
                    continue
                for partner in c['partner_config'].keys():
                    unsigned_path = '%s/unsigned/partner-repacks/%s/%s/%s/%s' % (dirs['abs_work_dir'], partner, platform, locale, installer_name)
                    signed_dir = '%s/partner-repacks/%s/%s/%s' % (dirs['abs_work_dir'], partner, platform, locale)
                    signed_path = "%s/%s" % (signed_dir, installer_name)
                    total_count += 1
                    self.info("Signing %s %s." % (platform, locale))
                    if not os.path.exists(unsigned_path):
                        self.error("Missing apk %s!" % unsigned_path)
                        continue
                    if self.sign_apk(unsigned_path, c['keystore'],
                                     self.store_passphrase, self.key_passphrase,
                                     c['key_alias']) != 0:
                        self.add_summary("Unable to sign %s:%s apk!" % (platform, locale), level=FATAL)
                    else:
                        self.mkdir_p(signed_dir)
                        if self.align_apk(unsigned_path, signed_path):
                            self.add_failure(platform, locale,
                                             message="Unable to align %(platform)s%(locale)s apk!")
                            self.rmtree(signed_dir)
                        else:
                            success_count += 1
        self.summarize_success_count(success_count, total_count,
                                     message="Signed %d of %d apks successfully.")

    # TODO verify signatures.

    def upload_signed_bits(self):
        self._upload(dir_name="partner-repacks")


# main {{{1
if __name__ == '__main__':
    mobile_partner_repack = MobilePartnerRepack()
    mobile_partner_repack.run_and_exit()