"""Let's Encrypt CLI."""
# TODO: Sanity check all input.  Be sure to avoid shell code etc...
import argparse
import atexit
import functools
import logging
import logging.handlers
import os
import pkg_resources
import sys
import time
import traceback

import configargparse
import configobj
import OpenSSL
import zope.component
import zope.interface.exceptions
import zope.interface.verify

from acme import client as acme_client
from acme import jose

import letsencrypt

from letsencrypt import account
from letsencrypt import colored_logging
from letsencrypt import configuration
from letsencrypt import constants
from letsencrypt import client
from letsencrypt import crypto_util
from letsencrypt import errors
from letsencrypt import interfaces
from letsencrypt import le_util
from letsencrypt import log
from letsencrypt import reporter
from letsencrypt import storage

from letsencrypt.display import util as display_util
from letsencrypt.display import ops as display_ops
from letsencrypt.plugins import disco as plugins_disco


logger = logging.getLogger(__name__)


# Argparse's help formatting has a lot of unhelpful peculiarities, so we want
# to replace as much of it as we can...

# This is the stub to include in help generated by argparse

SHORT_USAGE = """
  letsencrypt [SUBCOMMAND] [options] [domains]

The Let's Encrypt agent can obtain and install HTTPS/TLS/SSL certificates.  By
default, it will attempt to use a webserver both for obtaining and installing
the cert.  """

# This is the short help for letsencrypt --help, where we disable argparse
# altogether
USAGE = SHORT_USAGE + """Major SUBCOMMANDS are:

  (default) everything Obtain & install a cert in your current webserver
  auth                 Authenticate & obtain cert, but do not install it
  install              Install a previously obtained cert in a server
  revoke               Revoke a previously obtained certificate
  rollback             Rollback server configuration changes made during install
  config_changes       Show changes made to server config during installation

Choice of server for authentication/installation:

  --apache          Use the Apache plugin for authentication & installation
  --nginx           Use the Nginx plugin for authentication & installation
  --standalone      Run a standalone HTTPS server (for authentication only)
  OR:
  --authenticator standalone --installer nginx

More detailed help:

  -h, --help [topic]    print this message, or detailed help on a topic;
                        the available topics are:

   all, apache, automation, nginx, paths, security, testing, or any of the
   subcommands
"""


def _find_domains(args, installer):
    if args.domains is None:
        domains = display_ops.choose_names(installer)
    else:
        domains = args.domains

    if not domains:
        raise errors.Error("Please specify --domains, or --installer that "
                           "will help in domain names autodiscovery")

    return domains


def _determine_account(args, config):
    """Determine which account to use.

    In order to make the renewer (configuration de/serialization) happy,
    if ``args.account`` is ``None``, it will be updated based on the
    user input. Same for ``args.email``.

    :param argparse.Namespace args: CLI arguments
    :param letsencrypt.interface.IConfig config: Configuration object
    :param .AccountStorage account_storage: Account storage.

    :returns: Account and optionally ACME client API (biproduct of new
        registration).
    :rtype: `tuple` of `letsencrypt.account.Account` and
        `acme.client.Client`

    """
    account_storage = account.AccountFileStorage(config)
    acme = None

    if args.account is not None:
        acc = account_storage.load(args.account)
    else:
        accounts = account_storage.find_all()
        if len(accounts) > 1:
            acc = display_ops.choose_account(accounts)
        elif len(accounts) == 1:
            acc = accounts[0]
        else:  # no account registered yet
            if args.email is None:
                args.email = display_ops.get_email()
            if not args.email:  # get_email might return ""
                args.email = None

            def _tos_cb(regr):
                if args.tos:
                    return True
                msg = ("Please read the Terms of Service at {0}. You "
                       "must agree in order to register with the ACME "
                       "server at {1}".format(
                           regr.terms_of_service, config.server))
                return zope.component.getUtility(interfaces.IDisplay).yesno(
                    msg, "Agree", "Cancel")

            try:
                acc, acme = client.register(
                    config, account_storage, tos_cb=_tos_cb)
            except errors.Error as error:
                logger.debug(error, exc_info=True)
                raise errors.Error(
                    "Unable to register an account with ACME server")

    args.account = acc.id
    return acc, acme


def _init_le_client(args, config, authenticator, installer):
    if authenticator is not None:
        # if authenticator was given, then we will need account...
        acc, acme = _determine_account(args, config)
        logger.debug("Picked account: %r", acc)
        # XXX
        #crypto_util.validate_key_csr(acc.key)
    else:
        acc, acme = None, None

    return client.Client(config, acc, authenticator, installer, acme=acme)


def _find_duplicative_certs(domains, config, renew_config):
    """Find existing certs that duplicate the request."""

    identical_names_cert, subset_names_cert = None, None

    configs_dir = renew_config.renewal_configs_dir
    le_util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid())

    cli_config = configuration.RenewerConfiguration(config)
    for renewal_file in os.listdir(configs_dir):
        try:
            full_path = os.path.join(configs_dir, renewal_file)
            rc_config = configobj.ConfigObj(renew_config.renewer_config_file)
            rc_config.merge(configobj.ConfigObj(full_path))

            rc_config.filename = full_path
            candidate_lineage = storage.RenewableCert(
                rc_config, config_opts=None, cli_config=cli_config)
        except (configobj.ConfigObjError, errors.CertStorageError, IOError):
            logger.warning("Renewal configuration file %s is broken. "

                           "Skipping.", full_path)
            continue
        # TODO: Handle these differently depending on whether they are
        #       expired or still valid?
        candidate_names = set(candidate_lineage.names())
        if candidate_names == set(domains):
            identical_names_cert = candidate_lineage
        elif candidate_names.issubset(set(domains)):
            subset_names_cert = candidate_lineage

    return identical_names_cert, subset_names_cert


def run(args, config, plugins):  # pylint: disable=too-many-branches,too-many-locals
    """Obtain a certificate and install."""
    if args.configurator is not None and (args.installer is not None or
                                          args.authenticator is not None):
        return ("Either --configurator or --authenticator/--installer"
                "pair, but not both, is allowed")

    if args.authenticator is not None or args.installer is not None:
        installer = display_ops.pick_installer(
            config, args.installer, plugins)
        authenticator = display_ops.pick_authenticator(
            config, args.authenticator, plugins)
    else:
        # TODO: this assumes that user doesn't want to pick authenticator
        #       and installer separately...
        authenticator = installer = display_ops.pick_configurator(
            config, args.configurator, plugins)

    if installer is None or authenticator is None:
        return "Configurator could not be determined"

    domains = _find_domains(args, installer)

    treat_as_renewal = False

    # Considering the possibility that the requested certificate is
    # related to an existing certificate.  (config.duplicate, which
    # is set with --d