#!/usr/bin/env python

import os
import logging
import argparse
import json
import re
from azure_key_vault_report import azure_key_vault_report
from azure_key_vault_report import az_cmd
from cert_report import certs as crt
from cert_report import report as crt_report
from message_handler import message_handler
from .slack_workflow_report import slack_workflow_report
from .post_payloads import post_payloads


########################################################################################################################


class Monitoring(object):
    def __init__(self, args):
        self.args = args
        self.report_full = {}
        self.kv = None
        self.success = False
        self.payloads = []
        self.ms_teams_payload = ""
        self.slack_workflow_posts = []
        self.slack_app = None
        self.teams_output = None
        self.msg_handler = None
        self.certs = None

    def set_webhooks(self, webhook):
        if "slack.com/services" in str(webhook):
            logging.info("Slack services webhook detected.")
            self.slack_app = True
        elif "slack.com" not in str(webhook):
            logging.info("No slack webhook detected. Assuming post to MS Teams.")
            self.teams_output = True

        if str(webhook).startswith("https"):
            self.msg_handler = message_handler.MessageHandler(webhook)

    def key_vault_report(self, vaults=None):
        # Return if no vaults to parse
        if not vaults:
            logging.error("No vaults provided.")
            return

        # If only one key vault to check, ensure it is treated as a list
        if isinstance(vaults, str):
            vaults = [vaults]

        # Runs all the az key vaults commands and add the results to the 'az_results' list
        az_results = []
        for vault in vaults:
            az_results += az_cmd.az_cmd(vault, self.args.record_types)

        # The report is generated by using the pip package ops-py-azure-key-vault-report
        # If argument 'include_no_expiration' is not provided, then the variable
        # 'ignore_no_expiration' is then set to True
        self.kv = azure_key_vault_report.AzureKeyVaultReport(az_results)
        self.kv.parse_results()
        self.kv.add_summary()
        self.kv.add_report(expire_threshold=self.args.expire_threshold,
                           ignore_no_expiration=self.args.include_no_expiration,
                           include_all=self.args.include_all,
                           teams_json=self.teams_output,
                           alert_threshold=self.args.alert_threshold)

    def report_full_json(self):
        if self.kv:
            self.report_full = self.kv.get_report_full()
            workflow_output_name = str(WORKFLOW_OUTPUT_NAME).strip().lower().replace(" ", "_")[:40]
            self.report_full["name"] = workflow_output_name
            self.report_full["repository_name"] = str(GITHUB_REPOSITORY).split("/")[-1]
            self.report_full["client_id"] = AZURE_CLIENT_ID
            self.report_full["subscription_id"] = AZURE_SUBSCRIPTION_ID
            self.report_full["tenant_id"] = AZURE_TENANT_ID

        self.report_full["ssl_certs"] = self.certs

    def write_report_full_json(self, workflow_output_file):
        if self.report_full:
            # Ensure a valid filename is set. If that is not the case, then 'output.json' is used as default.
            workflow_output_file = str(workflow_output_file.lower()).replace(" ", "_")
            if not bool(re.match("^[a-z0-9_-]*$", workflow_output_file)):
                workflow_output_file = "output.json"

            # Write full report as json to file
            with open(workflow_output_file, 'w') as f:
                json.dump(self.report_full, f, default=str)

    def alert(self):
        if isinstance(self.args.alert_threshold, int):
            # If the webhook is previously determined to be an MS Teams webhook, then get the teams alert payloads
            if self.teams_output:
                payloads = self.kv.get_teams_payloads(self.args.title, alert=True)
                if payloads:
                    self.payloads += payloads

            # If the webhook is previously determined to be a Slack App webhook, then get the slack alert payloads
            if self.slack_app:
                payloads = self.kv.get_slack_payloads(self.args.title, max_chars=self.args.slack_split_chars,
                                                      alert=True)
                if payloads:
                    self.payloads += payloads

            if not self.slack_app and not self.teams_output:
                logging.error("Alerts to Slack Workflow not supported. Please use Slack App")

    def report(self):
        # If the webhook is previously determined to be a Slack App webhook, then get the slack report payloads
        if self.slack_app:
            payloads = self.kv.get_slack_payloads(self.args.title, max_chars=self.args.slack_split_chars)
            if payloads:
                self.payloads += payloads

        # If Slack Workflow, then get the slack report Workflow payloads
        if not self.slack_app and not self.teams_output:
            self.slack_workflow_posts = self.kv.get_slack_payloads(self.args.title, max_chars=self.args.slack_split_chars, app=False)

        # If the webhook is previously determined to be an MS Teams webhook, the teams_report function is called
        # which also will post the report message
        if self.teams_output:
            payload = self.kv.get_teams_payloads(self.args.title)

            if not payload:
                logging.error("Unable to retrieve MS Teams payload")
                return

            # If stdout_only the full MS Teams payload is returned
            if self.args.stdout_only:
                return payload

            # If payload is too large with the report as html, a new payload is generated.
            # A custom text is provided instead, which will be used instead of the html report.
            if len(payload) > self.args.teams_max_chars:
                warning_msg = f"The {self.args.title} length is above the character limit count of {self.args.teams_max_chars}"
                logging.warning(warning_msg)
                title = f"WARNING! {warning_msg}"
                text = "The html report have been omitted from the report due to size limits."
                payload = self.kv.get_teams_payloads(title, text=text)

            self.payloads += [payload]

    def ssl_cert(self, ssl_certs=None):
        # Return if no vaults to parse
        if not ssl_certs:
            logging.error("No SSL certs provided.")
            return

        # If only one key vault to check, ensure it is treated as a list
        if isinstance(ssl_certs, str):
            ssl_certs = [ssl_certs]

        if ssl_certs:
            c = crt.Certs(ssl_certs)
            c.parse_certs()
            self.certs = c.get_certs()

    def ssl_cert_report(self):
        # Return if no vaults to parse
        if not self.certs:
            return

        if self.slack_app:
            s = crt_report.Report(self.certs, self.args.ssl_warning_threshold, self.args.ssl_critical_threshold,
                                  skip_ok=self.args.ssl_include_ok)
            s.gen_report()
            rows = s.get_report()
            if rows:
                self.payloads += [{"text": rows}]

        if self.teams_output:
            s = crt_report.Report(self.certs, self.args.ssl_warning_threshold, self.args.ssl_critical_threshold,
                                  skip_ok=self.args.ssl_include_ok, html=True)
            s.gen_report()
            title, rows = s.get_report()
            if title and rows:
                payload = {
                    "@type": "MessageCard",
                    "@context": "http://schema.org/extensions",
                    "themeColor": "0076D7",
                    "summary": "-",
                    "sections": [
                        {
                            "activityTitle": title,
                            "activitySubtitle": "",
                            "activityImage": "",
                            "markdown": True
                        },
                        {
                            "startGroup": True,
                            "text": rows
                        }
                    ]
                }
                self.payloads += [payload]

        if not self.slack_app and not self.teams_output:
            logging.error("SSL reports to Slack Workflow is not supported atm. Please use Slack App")

    def post_payloads(self):
        # If payloads, the slack_post function is called to post the report payloads
        if self.payloads and self.msg_handler and not self.args.stdout_only:
            self.success = post_payloads(self.msg_handler, self.payloads)

        if self.slack_workflow_posts and not self.args.stdout_only:
            # The slack_post function is called to post the Slack Workflow reports payloads
            self.success = slack_workflow_report(self.msg_handler, posts=self.slack_workflow_posts)

    def get_md_report(self):
        return self.kv.get_report_summary_markdown()

    def get_success(self):
        return self.success


def main():
    logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO)

    # The list of key vaults to check passed as command line arguments
    parser = argparse.ArgumentParser()
    parser.add_argument("-v", "--vaults", nargs='+',
                        help="List of key vaults to check. E.g. kv-dev kv-test")

    parser.add_argument("-c", "--alert_threshold", type=int,
                        help="If set then only records that are +/- this value in days till expire/expired "
                             "will be alerted. Records will be alerted as individual Slack messages. "
                             "Summary report and other reports will not be posted.")

    parser.add_argument("-e", "--expire_threshold", type=int,
                        help="If a value (int) is set. The days to the record's Expiration Date must be below "
                             "this threshold in order to be included in the report (Default: not set).")

    parser.add_argument("-a", "--include_all", action='store_true',
                        help="Include all records in output (verbose) if provided.")

    parser.add_argument("-i", "--include_no_expiration", action='store_false',
                        help="Also include records which has no Expiration Date set.")

    parser.add_argument("-T", "--title", type=str, default="Azure Key Vault report",
                        help="The title of the message posted in Slack or MS Teams")

    parser.add_argument("-r", "--record_types", nargs='+',
                        help="List of record types to check for. E.g. certificate secret\n"
                             "Valid types are: certificate secret key\n"
                             "Default is all: certificate secret key",
                        default="certificate secret key")

    parser.add_argument("-L", "--slack_split_chars", type=int, default=3500,
                        help="Slack message above this value will be split into multiple post messages.")

    parser.add_argument("-M", "--teams_max_chars", type=int, default=17367,
                        help="The max characters the report can have due to the MS Teams payload size limits")

    parser.add_argument("-S", "--stdout_only", action='store_true',
                        help="Only print report to stdout. No post to messagehandler (Slack or MS Teams")

    parser.add_argument("-w", "--workflow_output_file", type=str, default="output.json",
                        help="The file where the full json report will be written.")

    parser.add_argument("-s", "--silence", action='store_true',
                        help="If provided the workflow will run and log, but no messages to Slack or MS Teams and "
                             "no print to stdout.")

    parser.add_argument("-l", "--ssl_certs", nargs='+',
                        help="List of ssl certs to check. E.g. kv-dev kv-test")

    parser.add_argument("-R", "--ssl_include_ok", action='store_false',
                        help="If provided all SSL certs will included in the report")

    parser.add_argument("-W", "--ssl_warning_threshold", type=int, default=45,
                        help="The SSL cert expire days warning threshold.")

    parser.add_argument("-C", "--ssl_critical_threshold", type=int, default=14,
                        help="The SSL cert expire days critical threshold.")

    parser.add_argument("-t", "--report_types", nargs='+', default=["kv_report"],
                        help="Kind of report to be posted: kv_report kv_alert cert_report")

    args = parser.parse_args()

    # Log each argparse argument
    for k, v in sorted(vars(args).items()):
        logging.info(f"Argument '{k}': '{v}'")

    ####################################################################################################################

    # Determines if a webhook is set, and if it is for a Slack App or a Slack Workflow. If not we assume for MS Teams.
    if not WEBHOOK_REPORT:
        logging.warning("'WEBHOOK_REPORT' not provided. Messages will not be posted to the message handler.")

    monitoring = Monitoring(args)
    monitoring.set_webhooks(WEBHOOK_REPORT)

    monitoring.key_vault_report(vaults=args.vaults)
    monitoring.ssl_cert(ssl_certs=args.ssl_certs)

    monitoring.report_full_json()
    monitoring.write_report_full_json(args.workflow_output_file)

    # If the 'silence' argument is provided, then we are done once the json output file is written
    if args.silence:
        return

    # If the 'stdout_only' argument is provided, the plain text markdown reports are printed to stdout, and then exit.
    # If 'teams_output', the 'stdout_only' is handled by the 'teams_alert' function instead.
    if args.stdout_only and not monitoring.teams_output:
        print(args.title)
        print(monitoring.get_md_report())
        return

    if args.report_types:
        report_types = args.report_types
        if isinstance(args.report_types, str):
            report_types = args.report_types.split()

        # Do the reporting, alerting, ssl certs checks...
        if 'kv_report' in report_types:
            out = monitoring.report()
            if out:
                print(out)

        if 'kv_alert' in report_types:
            monitoring.alert()

        if 'cert_report' in report_types:
            monitoring.ssl_cert_report()

        monitoring.post_payloads()

    # If success and 'WEBHOOK_NOTIFY' is provided
    # an additional notify will be posted to the 'WEBHOOK_NOTIFY' webhook
    if monitoring.get_success() and WEBHOOK_NOTIFY:
        logging.info(f"Trigger additional alert about new report message(s)...")
        alert = message_handler.MessageHandler(WEBHOOK_NOTIFY)
        alert.post_payload()


########################################################################################################################


if __name__ == '__main__':
    # The actual report will be posted to the webhook exported in
    # the following environment variable
    WEBHOOK_REPORT = os.getenv("WEBHOOK_REPORT")

    # When all the reports have been posted, an additional POST is performed
    # to the webhook exported in following environment variable:
    WEBHOOK_NOTIFY = os.getenv("WEBHOOK_NOTIFY")

    # The value of the name key in the full json logfile
    WORKFLOW_OUTPUT_NAME = os.getenv("WORKFLOW_OUTPUT_NAME", "")

    # The value of the github_repo name key in the full json logfile
    GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY", "")

    # These Azure environment variables will be used in the full json logfile
    AZURE_CLIENT_ID = os.getenv("AZURE_CLIENT_ID")
    AZURE_SUBSCRIPTION_ID = os.getenv("AZURE_SUBSCRIPTION_ID")
    AZURE_TENANT_ID = os.getenv("AZURE_TENANT_ID")

    main()
