#!/usr/bin/env python3
# TODO: config file validation (PreCommitJobsExecutor method to call in __enter__)
# TODO: total-time setting (total time at the end of pre-commit)
# TODO: quiet-mode setting (no output)
# TODO: run commands via subprocess, not system(cmd)
# TODO: automatic check for updates
import subprocess
import sys
from dataclasses import dataclass
from time import sleep
from typing import Any, Dict

import yaml


# DataClasses
# ----------------------------------------------------------------------
@dataclass(init=False, frozen=True)
class Color:
    BLUE: str = '\033[1;34m'
    GREEN: str = '\033[1;32m'
    RED: str = '\033[1;31m'
    CYAN: str = '\033[1;36m'
    PURPLE: str = '\033[1;35m'
    NO_COLOR: str = '\033[0m'


@dataclass(init=False, frozen=True)
class Constant:
    CFG_FILEPATH: str = "nugit.yaml"
    OUTPUT_HEADER: str = (f"\n{Color.PURPLE}{'- nuGit -'.center(28)}\n"
                          f"- Pre-commit Solution Tool -\n\n")
    OUTPUT_FOOTER: str = f"\n{Color.GREEN}{'- nuGit jobs done -'.center(28)}{Color.NO_COLOR}\n\n"


# Exceptions
# ----------------------------------------------------------------------
class PreCommitJobError(Exception):
    pass


class PreCommitError(Exception):
    pass


# Functions
# ----------------------------------------------------------------------
def parse_yaml(filepath: str) -> Dict[str, Any]:
    with open(filepath, "r") as file:
        data = yaml.safe_load(file)
    return data


def run_job(job_name: str, job_attrs: Dict[str, Any]) -> None:
    quite = job_attrs.get("quite", False)
    required = job_attrs.get("required", False)
    to_run = job_attrs.get("run", [])
    if not isinstance(required, bool):
        raise PreCommitError(f"`required` for {job_name} is not bool.")
    if not to_run:
        raise PreCommitError(f"`run` for {job_name} not defined.")

    sys.stderr.write(f"{Color.RED if required else Color.BLUE}"
                     f"({'req' if required else 'opt'}) {Color.NO_COLOR}"
                     f"{'(quite) ' if quite else ''}"
                     f"[{Color.BLUE}{job_name}{Color.NO_COLOR}]\n")

    for cmd in to_run:
        if not cmd:
            raise PreCommitJobError(f"empty command on {job_name}.")
        p = subprocess.Popen(
            cmd, shell=True,
            stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )
        out, err = p.communicate()

        if not quite or p.returncode != 0:
            if out:
                for line in out.splitlines():
                    sys.stdout.write(f"\t{Color.CYAN}>>> {Color.NO_COLOR}{line.decode()}\n")
                sys.stderr.write("\n")
            if err:
                for line in err.splitlines():
                    sys.stderr.write(f"\t{Color.CYAN}>>> {Color.NO_COLOR}{line.decode()}\n")
                sys.stderr.write("\n")
            if required and p.returncode != 0:
                raise PreCommitJobError(f"{job_name} failed.")


# Executor
# ----------------------------------------------------------------------
class PreCommitJobsExecutor:
    def __init__(self, config_filepath: str) -> None:
        self.jobs = None
        self.settings = None
        self.filepath = config_filepath

    def parse_config(self):
        data = parse_yaml(self.filepath)
        self.settings = data.get("settings", {})
        self.jobs = data.get("jobs", {})

    def run_jobs(self) -> None:
        sys.stdout.write(Constant.OUTPUT_HEADER)
        timeout = self.settings.get("timeout", 0)
        if not isinstance(timeout, (int, float)):
            raise PreCommitError("wrong settings.timeout type.")

        for job_name, job_attrs in self.jobs.items():
            run_job(job_name, job_attrs)
            sleep(timeout)
        sys.stdout.write(Constant.OUTPUT_FOOTER)

    def __enter__(self):
        self.parse_config()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type in (PreCommitJobError, PreCommitError):
            sys.exit(f"{Color.RED}{exc_type.__name__}: {exc_value}{Color.NO_COLOR}\n")


# Run
# ----------------------------------------------------------------------
if __name__ == "__main__":
    with PreCommitJobsExecutor(Constant.CFG_FILEPATH) as pc:
        pc.run_jobs()
