#!/usr/bin/env python
# -*- encoding: utf-8 -*-
import argparse
import logging
import os
import re
import signal
import sys
import tempfile
import time
import traceback
from random import random
from typing import List, Optional

import coloredlogs
import schedule
import yaml
from certbot.compat import misc

from dnsrobocert.core import certbot, config, legacy, utils

LOGGER = logging.getLogger(__name__)
coloredlogs.install(logger=LOGGER)


def _process_config(config_path: str, directory_path: str, runtime_config_path: str):
    dnsrobocert_config = config.load(config_path)

    if not dnsrobocert_config:
        return

    if dnsrobocert_config.get("draft"):
        LOGGER.info("Configuration file is in draft mode: no action will be done.")
        return

    with open(runtime_config_path, "w") as f:
        f.write(yaml.dump(dnsrobocert_config))

    utils.configure_certbot_workspace(dnsrobocert_config, directory_path)

    LOGGER.info("Registering ACME account if needed.")
    certbot.account(runtime_config_path, directory_path)

    LOGGER.info("Creating missing certificates if needed (~1min for each)")
    certificates = dnsrobocert_config.get("certificates", {})
    for certificate in certificates:
        try:
            lineage = config.get_lineage(certificate)
            domains = certificate["domains"]
            force_renew = certificate.get("force_renew", False)
            LOGGER.info(
                "Handling the certificate for domain(s): {0}".format(", ".join(domains))
            )
            certbot.certonly(
                runtime_config_path,
                directory_path,
                lineage,
                domains,
                force_renew=force_renew,
            )
        except BaseException as error:
            LOGGER.error(
                "An error occurred while processing certificate config `{0}`:\n{1}".format(
                    certificate, error
                )
            )

    LOGGER.info("Revoke and delete certificates if needed")
    lineages = {config.get_lineage(certificate) for certificate in certificates}
    for domain in os.listdir(os.path.join(directory_path, "live")):
        if domain != "README":
            domain = re.sub(r"^\*\.", "", domain)
            if domain not in lineages:
                LOGGER.info("Removing the certificate {0}".format(domain))
                certbot.revoke(runtime_config_path, directory_path, domain)


class _Daemon:
    _do_shutdown = False

    def __init__(self):
        signal.signal(signal.SIGINT, self.shutdown)
        signal.signal(signal.SIGTERM, self.shutdown)

    def shutdown(self, _signum, _frame):
        self._do_shutdown = True

    def do_shutdown(self):
        return self._do_shutdown


def _renew_job(config_path: str, directory_path: str):
    random_delay_seconds = 21600  # Random delay up to 12 hours
    wait_time = int(random() * random_delay_seconds)
    LOGGER.info("Automated execution: renew certificates if needed.")
    LOGGER.info("Random wait for this execution: {0} seconds".format(wait_time))
    time.sleep(wait_time)
    certbot.renew(config_path, directory_path)


def _watch_config(config_path: str, directory_path: str):
    LOGGER.info("Starting DNSroboCert.")

    with tempfile.TemporaryDirectory() as workspace:
        runtime_config_path = os.path.join(workspace, "dnsrobocert-runtime.yml")

        schedule.every().day.at("12:00").do(
            _renew_job, config_path=runtime_config_path, directory_path=directory_path
        )
        schedule.every().day.at("00:00").do(
            _renew_job, config_path=runtime_config_path, directory_path=directory_path
        )

        daemon = _Daemon()
        previous_digest = ""
        while not daemon.do_shutdown():
            schedule.run_pending()

            try:
                generated_config_path = legacy.migrate(config_path)
                effective_config_path = (
                    generated_config_path if generated_config_path else config_path
                )
                digest = utils.digest(effective_config_path)

                if digest != previous_digest:
                    previous_digest = digest
                    _process_config(
                        effective_config_path, directory_path, runtime_config_path
                    )
            except BaseException as error:
                LOGGER.error("An error occurred during DNSroboCert watch:")
                LOGGER.error(error)
                traceback.print_exc(file=sys.stderr)

            time.sleep(1)

    LOGGER.info("Exiting DNSroboCert.")


def main(args: Optional[List[str]] = None):
    if not args:
        args = sys.argv[1:]

    parser = argparse.ArgumentParser(description="Start dnsrobocert.")
    parser.add_argument(
        "--config",
        "-c",
        default=os.path.join(os.getcwd(), "dnsrobocert.yml"),
        help="Set the dnsrobocert config to use.",
    )
    parser.add_argument(
        "--directory",
        "-d",
        default=misc.get_default_folder("config"),
        help="Set the directory path where certificates are stored.",
    )

    parsed_args = parser.parse_args(args)

    _watch_config(
        os.path.abspath(parsed_args.config), os.path.abspath(parsed_args.directory)
    )


if __name__ == "__main__":
    main()
