# automatically generated by auxilium
# pylint: disable=too-many-lines
import collections
import contextlib
import dataclasses as dc
import enum
import subprocess
import time
import typing as tp
from pathlib import Path

from ._payload._docker_executors import subprocess_run


@dc.dataclass(frozen=True)
class Job:
    name: str
    stage: str = ""

    @property
    def ident(self) -> str:
        if self.stage:
            return f"{self.stage}:{self.name}"
        return self.name

    def is_up_to_date(
        self,
        env: tp.Mapping[str, tp.Any],  # pylint: disable=unused-argument
    ) -> bool:
        return True

    def is_included(
        self,
        env: tp.Mapping[str, tp.Any],  # pylint: disable=unused-argument
    ) -> tp.Tuple[bool, str]:
        return True, "by default"

    def script(self) -> None:
        raise NotImplementedError()


@dc.dataclass(frozen=True)
class _TestingJobMixinState:
    log: tp.List[str] = dc.field(default_factory=list)


@dc.dataclass(frozen=True)
class TestingJobMixin(_TestingJobMixinState, Job):
    def script(self) -> None:
        if "fail" in self.ident:
            self.log.append(f"failing {self.ident}")
            raise RuntimeError(f"failing {self.ident}")
        self.log.append(self.ident)


@dc.dataclass(frozen=True)
class DependentCheckMixin(Job):  # pylint: disable=abstract-method
    dependent: tp.OrderedDict[str, tp.Any] = dc.field(
        default_factory=collections.OrderedDict
    )

    def is_up_to_date(self, env: tp.Mapping[str, tp.Any]) -> bool:
        reason = None
        for key, val in self.dependent.items():
            reason = getattr(self, f"_check_{key}")(env, val)
            if reason is not None:
                if env.get("verbose", False):
                    print(f"{self.ident:>30} outdated bc {reason}", flush=True)
                return False
        return super().is_up_to_date(env)

    @staticmethod
    def _check_up_to_date(
        env: tp.Mapping[str, tp.Any], val: bool  # pylint: disable=unused-argument
    ) -> tp.Optional[str]:
        if not val:
            return f"up_to_date={val}"
        return None

    @staticmethod
    def _check_files(
        env: tp.Mapping[str, tp.Any], paths: tp.List[Path]
    ) -> tp.Optional[str]:
        for path in paths:
            out = subprocess_run(
                ["git", "diff", "HEAD", "HEAD~1", path],
                cwd=env["cwd"],
                capture_output=True,
                check=True,
            )
            diff = out.stdout.decode("utf-8").strip()
            if diff:
                return f"file-diff in {path}"
        return None


@dc.dataclass(frozen=True)
class TriggerMixin(DependentCheckMixin):  # pylint: disable=abstract-method
    trigger: tp.OrderedDict[str, tp.Any] = dc.field(
        default_factory=collections.OrderedDict
    )

    def is_included(self, env: tp.Mapping[str, tp.Any]) -> tp.Tuple[bool, str]:
        if not self.trigger:
            return super().is_included(env)

        for key, val in self.trigger.items():
            if key in "files":
                continue
            reason = getattr(self, f"_check_{key}")(env, val)
            if reason is not None:
                return True, reason
        return False, ""

    @staticmethod
    def _check_env(
        env: tp.Mapping[str, tp.Any], vals: tp.Dict[str, tp.Any]
    ) -> tp.Optional[str]:
        for key, val in vals.items():
            if env[key] == val:
                return f"{key} == {val}"
        return None

    @staticmethod
    def _check_env_bool(env: tp.Mapping[str, tp.Any], args: str) -> tp.Optional[str]:
        for key in args:
            if env[key]:
                return f"{key} == True"
        return None

    @staticmethod
    def _check_env_and(
        env: tp.Mapping[str, tp.Any], vals: tp.Dict[str, tp.Any]
    ) -> tp.Optional[str]:
        incl = True
        for key, val in vals.items():
            incl &= env[key] == val
        if incl:
            return " & ".join([f"{k} == {v}" for k, v in vals.items()])
        return None

    @staticmethod
    def _check_env_bool_and(
        env: tp.Mapping[str, tp.Any], vals: tp.List[str]
    ) -> tp.Optional[str]:
        incl = True
        for key in vals:
            incl &= env[key]
        if incl:
            return " & ".join([f"{k}" for k in vals]) + " == True"
        return None


class AutoName(enum.Enum):
    def _generate_next_value_(name: str, start, count, last_values) -> str:  # type: ignore  # pylint: disable=no-self-argument
        return name.lower()  # pylint: disable=no-member


class StateEnum(AutoName):
    CREATED = enum.auto()
    PENDING = enum.auto()
    UP_TO_DATE = enum.auto()
    SUCCESS = enum.auto()
    SKIPPED = enum.auto()
    FAILED = enum.auto()
    EXCLUDED = enum.auto()


class State:
    status: StateEnum = StateEnum.CREATED

    def __str__(self) -> str:
        return str(self.status.value)

    def done(self) -> bool:
        return self.status not in [
            StateEnum.CREATED,
            StateEnum.PENDING,
            StateEnum.EXCLUDED,
        ]

    def successful(self) -> bool:
        return self.status in [
            StateEnum.UP_TO_DATE,
            StateEnum.SUCCESS,
            StateEnum.EXCLUDED,
        ]

    def set_done(self, err: tp.Any = None) -> None:
        if err:
            self.status = StateEnum.FAILED
        else:
            self.status = StateEnum.SUCCESS

    @property
    def status_colored(self) -> str:
        if self.status == StateEnum.FAILED:
            col = 1
        elif self.status == StateEnum.SUCCESS:
            col = 2
        elif self.status == StateEnum.SKIPPED:
            col = 3
        elif self.status == StateEnum.UP_TO_DATE:
            col = 6
        else:
            return self.status.value
        return f"\033[1;3{col}m{self.status.value}\033[0m"


@dc.dataclass(frozen=True)
class DoubleLinkedListNode(Job):  # pylint: disable=abstract-method
    state: State = dc.field(default_factory=State)
    children: tp.List["DoubleLinkedListNode"] = dc.field(default_factory=list)
    parents: tp.List["DoubleLinkedListNode"] = dc.field(default_factory=list)

    @property
    def included_parents(self) -> tp.Iterator["DoubleLinkedListNode"]:
        for parent in self.parents:
            if parent.state.status != StateEnum.EXCLUDED:
                yield parent

    @property
    def included_children(self) -> tp.Iterator["DoubleLinkedListNode"]:
        for child in self.children:
            if child.state.status != StateEnum.EXCLUDED:
                yield child

    def __post_init__(self) -> None:
        for parent in self.parents:
            parent.children.append(self)
        for child in self.children:
            child.parents.append(self)

    def add_child(self, child: "DoubleLinkedListNode") -> None:
        self.children.append(child)
        child.parents.append(self)

    def remove_child(self, child: "DoubleLinkedListNode") -> None:
        self.children.remove(child)
        child.parents.remove(self)

    def unlink(self) -> None:
        for parent in reversed(self.parents):
            parent.remove_child(self)
        for child in reversed(self.children):
            self.remove_child(child)

    def is_up_to_date(self, env: tp.Mapping[str, tp.Any]) -> bool:
        for parent in self.parents:
            if parent.state.status == StateEnum.PENDING:
                return False
        return super().is_up_to_date(env)

    def skip_children(self) -> None:
        for child in self.included_children:
            child.state.status = StateEnum.SKIPPED
            child.skip_children()


@dc.dataclass(frozen=True)
class CommonJob(
    TriggerMixin, DependentCheckMixin, DoubleLinkedListNode, Job
):  # pylint: disable=abstract-method
    pass


T = CommonJob  # pylint: disable=invalid-name


@dc.dataclass
class JobManager:
    jobs: tp.OrderedDict[str, T] = dc.field(default_factory=collections.OrderedDict)
    env: tp.Dict[str, tp.Any] = dc.field(default_factory=dict)

    @property
    def included_jobs(self) -> tp.Iterator[T]:
        for job in self.jobs.values():
            if job.state.status != StateEnum.EXCLUDED:
                yield job

    def load_env(self, **kwgs: tp.Any) -> None:
        self.env.update(kwgs)
        self.env.setdefault("cwd", Path.cwd())

    def add_job(self, cls: tp.Type[T], **kwgs: tp.Any) -> T:
        job: T = cls(**kwgs)
        key = job.ident
        if key in self.jobs:
            raise RuntimeError(f"the job {key} is already present!")
        self.jobs[key] = job
        return job

    @property
    def root_jobs(self) -> tp.List[T]:
        return [job for job in self.included_jobs if not list(job.included_parents)]

    def run_if_needed(self, job: T) -> None:
        if job.state.done():
            return
        with self.collapsable_section(job):
            if job.state.status == StateEnum.CREATED:
                self.check_job_necessity(job)

            if job.state.status == StateEnum.PENDING:
                try:
                    job.script()
                    job.state.set_done()
                except (RuntimeError, subprocess.CalledProcessError) as err:
                    if self.env.get("verbose", False):
                        print(err)
                    job.state.set_done(err=err)
                    job.skip_children()

    def check_job_necessity(self, job: T) -> None:
        if job.is_up_to_date(self.env):
            job.state.status = StateEnum.UP_TO_DATE
        else:
            job.state.status = StateEnum.PENDING

    def show_result(self) -> None:
        if self.has_failed_jobs():
            print("\033[1;31mSummary :(\033[0m")
        else:
            print("\033[1;32mSummary :)\033[0m")

        def show(job: T) -> None:
            print(f"{job.ident:>30} {job.state.status_colored}")

        self.recurse(show)

    def check_necessity(self) -> None:
        self.recurse(self.check_job_necessity)

    def run_pipeline(self, show: bool = False) -> None:
        self.prune_unincluded(show=show)
        self.recurse(self.run_if_needed)

    def recurse(
        self,
        fct: tp.Callable[[T], None],
    ) -> None:
        seen = []
        open_jobs: tp.List[T] = self.root_jobs
        all_vals = list(self.jobs.values())

        while open_jobs:
            job = open_jobs.pop(0)
            assert job not in seen
            fct(job)
            seen.append(job)

            for child in job.included_children:
                if child not in open_jobs:
                    assert isinstance(child, T)
                    open_jobs.append(child)

            open_jobs = list(sorted(list(open_jobs), key=all_vals.index))

    def run_single(self, ident: str) -> None:
        for job in self.included_jobs:
            if job.ident != ident:
                job.state.status = StateEnum.EXCLUDED

        self.env["run_single"] = True
        self.run_if_needed(self.jobs[ident])

    def has_failed_jobs(self) -> bool:
        for job in self.included_jobs:
            if not job.state.successful():
                return True
        return False

    def prune_unincluded(self, show: bool = False) -> None:
        for job in self.included_jobs:
            incl, reason = job.is_included(self.env)
            if incl:
                if show:
                    print(f"{job.ident:>30} \033[1;32mincluded\033[0m bc {reason}")
            else:
                job.state.status = StateEnum.EXCLUDED
                if show:
                    if reason:
                        print(f"{job.ident:>30} \033[1;33momitting\033[0m bc {reason}")
                    else:
                        print(f"{job.ident:>30} \033[1;35mexcluded\033[0m")

    def add_testing_job(self, name: str, **kwgs: tp.Any) -> T:
        job = self.add_job(TestingJob, name=name, log=self.env["log"], **kwgs)
        return job

    @contextlib.contextmanager
    def collapsable_section(self, job: T) -> tp.Iterator[None]:
        title = f"{job.ident}"
        sec = f'section_{job.ident.replace(":", "_")}'
        collapsed = "true"
        if self.env.get("run_single", False):
            collapsed = "false"
        print(
            f"\033[0Ksection_start:{int(time.time())}:{sec}[collapsed={collapsed}]\r\033[0K╔═ {title}",
            flush=True,
        )
        yield
        print(
            f"\033[0Ksection_end:{int(time.time())}:{sec}\r\033[0K╚═ {job.state.status_colored}",
            flush=True,
        )


@dc.dataclass(frozen=True)
class TestingJob(CommonJob, TestingJobMixin, _TestingJobMixinState, Job):
    pass
