import configparser
import logging
import shutil
import subprocess
import traceback
import zipfile
from pathlib import Path

from flask import Flask, jsonify, request
from werkzeug.utils import secure_filename


def try_run(
    list_args: list, msg: str, reraise=True, level=logging.DEBUG, **kwargs
) -> subprocess.CompletedProcess:
    """
    Runs subprocess.run in a try except with some default arguments ( which can be overriden by supplying kwargs )
            Parameters:
                    list_args (list): Subprocess list which will be passed to subprocess.run
                    msg (str): A meaningful error message to be raised during non-zero exit code
                    reraise (bool):
                        True: will raise an Exception with traceback on error
                        False: will log a warning on error
                    **kwargs: additional parameters to be passed to subprocess.run
            Defaults:
                    subprocess.run is called with the following arguments by default
                    stderr = subprocess.PIPE
                    stdout = subprocess.PIPE # to print to console, pass kwargs ( stdout = None )
                    check = True
            Returns:
                    sub (subprocess.CompletedProcess): Object with the completed process
                        or
                    None: if the subprocess failed and reraise=False
    """

    cur_level = logging.getLogger().getEffectiveLevel()
    if level > cur_level:
        logging.getLogger().setLevel(level)

    try:
        kwargs["stdout"]
    except KeyError:
        kwargs["stdout"] = subprocess.PIPE

    if kwargs.get("stderr") is None:
        kwargs["stderr"] = subprocess.PIPE
    if kwargs.get("check") is None:
        kwargs["check"] = True

    list_args = [str(i) if isinstance(i, Path) else i for i in list_args]

    sub = None
    try:
        sub = subprocess.run(list_args, **kwargs)
    except (FileNotFoundError, TypeError) as e:
        logging.debug("arguments to subprocess")
        logging.debug(list_args)
        logging.debug(msg)
        logging.debug(kwargs)
        raise Exception("Command not found") from e
    except subprocess.CalledProcessError as e:
        if e.stdout is not None:
            logging.debug("stdout: \n{}".format(e.stdout.decode()))
        if e.stderr is not None:
            logging.debug("stderr: \n{}".format(e.stderr.decode()))
        logging.debug("returncode: {}".format(e.returncode))
        if reraise:
            raise Exception(msg) from e
        else:
            logging.warning(msg)
            logging.debug(traceback.format_exc())

    logging.getLogger().setLevel(cur_level)
    return sub


app = Flask(__name__)
upload_dir = Path("upload")


@app.route("/parse-dumps", methods=["POST"])
def parse_dumps():
    logging.basicConfig(
        level=logging.DEBUG,
        format="[%(asctime)s][%(levelname)s] %(message)s",
        handlers=[logging.StreamHandler()],
    )

    name = request.form["name"]
    timestamp = request.form["timestamp"]

    if name == str(None):
        folder = upload_dir / timestamp
    else:
        folder = upload_dir / name

    try:
        shutil.rmtree(folder)
    except FileNotFoundError:
        pass

    folder.mkdir(parents=True, exist_ok=True)
    folder = folder.absolute().resolve()

    # save files
    f = request.files["file"]
    f.save(folder / secure_filename(f.filename))

    # get configs
    conf_file = Path(__file__) / "../config.ini"
    conf_file = conf_file.resolve()

    config = configparser.ConfigParser()
    config.read(conf_file)

    # init variables
    script_path = Path(config["default"]["analysis_script_path"])
    logging.debug(script_path)
    output_dir = folder / "analysis"
    log_dir = folder / "logs"

    # extract the zip
    with zipfile.ZipFile(folder / secure_filename(f.filename), "r") as zip_ref:
        zip_ref.extractall(log_dir)

    # remove the zip file
    (folder / secure_filename(f.filename)).unlink()

    # run analysis
    cmd = try_run(
        ["python3", str(script_path), "parse-dumps", str(log_dir), str(output_dir)],
        "Failed to run analysis",
        stdout=None,
        stderr=subprocess.STDOUT,
        reraise=False,
    )

    if cmd == None:
        return jsonify({"status": "fail"})

    with open(output_dir / "parsed_dumps" / "required_file.txt") as f:
        files = [i.strip() for i in f.readlines()]

    ret = {"status": "ok", "files": files}
    return jsonify(ret)


@app.route("/static-analysis", methods=["POST"])
def static_analysis():
    logging.basicConfig(
        level=logging.DEBUG,
        format="[%(asctime)s][%(levelname)s] %(message)s",
        handlers=[logging.StreamHandler()],
    )

    name = request.form["name"]
    timestamp = request.form["timestamp"]

    if name == str(None):
        folder = upload_dir / timestamp
    else:
        folder = upload_dir / name

    files_dir = folder / "files"

    files_dir.mkdir(parents=True, exist_ok=True)
    files_dir = files_dir.absolute().resolve()

    # save files
    f = request.files["file"]
    f.save(folder / secure_filename(f.filename))

    # get configs
    conf_file = Path(__file__) / "../config.ini"
    conf_file = conf_file.resolve()

    config = configparser.ConfigParser()
    config.read(conf_file)

    # init variables
    script_path = Path(config["default"]["analysis_script_path"])
    logging.debug(script_path)
    output_dir = folder / "analysis"

    # extract the zip
    with zipfile.ZipFile(folder / secure_filename(f.filename), "r") as zip_ref:
        zip_ref.extractall(files_dir)

    # remove the zip file
    (folder / secure_filename(f.filename)).unlink()

    # run analysis
    cmd = try_run(
        [
            "python3",
            str(script_path),
            "static-analysis",
            str(files_dir),
            str(output_dir),
        ],
        "Failed to run analysis",
        stdout=None,
        stderr=subprocess.STDOUT,
        reraise=False,
    )

    if cmd == None:
        ret = {"status": "fail"}
    else:
        ret = {"status": "ok"}
    return jsonify(ret)


@app.route("/mapping", methods=["POST"])
def mapping():
    logging.basicConfig(
        level=logging.DEBUG,
        format="[%(asctime)s][%(levelname)s] %(message)s",
        handlers=[logging.StreamHandler()],
    )

    name = request.form["name"]
    timestamp = request.form["timestamp"]

    if name == str(None):
        folder = upload_dir / timestamp
    else:
        folder = upload_dir / name

    # get configs
    conf_file = Path(__file__) / "../config.ini"
    conf_file = conf_file.resolve()

    config = configparser.ConfigParser()
    config.read(conf_file)

    # init variables
    script_path = Path(config["default"]["mapping_script_path"])
    public_ip = config["default"]["public_ip"]
    output_dir = folder / "analysis"

    # run analysis
    cmd = try_run(
        [
            "python3",
            str(script_path),
            str(output_dir),
        ],
        "Failed to run mapping",
        stdout=None,
        stderr=subprocess.STDOUT,
        reraise=False,
    )

    if cmd == None:
        ret = {"status": "fail"}
    else:
        ret = {"status": "ok", "url": "http://" + public_ip + ":5601"}

    return jsonify(ret)


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0")
