import glob
import shlex
import sys
from argparse import Namespace
from dataclasses import dataclass
from functools import wraps
from pathlib import Path
from subprocess import Popen
from typing import Any
from typing import Callable
from typing import NamedTuple


class CommandRunner:
    def __init__(self, shell: bool = False, print_cmd: bool = True,
                 exit_non_zero: bool = True, print_cmd_sep: bool = True) -> None:
        self.shell = shell
        self.print_cmd = print_cmd
        self.exit_non_zero = exit_non_zero
        self.print_cmd_sep = print_cmd_sep

    def run(self, cmd: str) -> int:
        if self.print_cmd_sep:
            print('=' * 80)

        if self.print_cmd:
            print(f'[CMD]: {cmd}')

        p = Popen(args=shlex.split(cmd), shell=self.shell)
        p.wait()

        if self.print_cmd_sep:
            print('=' * 80)

        if self.exit_non_zero and p.returncode != 0:
            print(f'Last command exited with non zero exit code. [{p.returncode}]')
            exit(1)

        return p.returncode

    def set_exit_non_zero(self, val: bool) -> None:
        self.exit_non_zero = val


@dataclass
class TargetDepends:
    targets: list[str]
    files: list[str]


@dataclass
class TargetCreates:
    files: list[str] | None
    func: Callable[[str], str] | None


class TargetData(NamedTuple):
    name: str
    depends: TargetDepends
    creates: TargetCreates


TargetCreateType = list[str] | Callable[[str], str]
TargetFuncType = Callable[[Namespace, TargetData], None]
WrapTargetFuncType = Callable[[Namespace], None]

_targets: dict[str, TargetData] = {}


def _parse_dependencies(depends: list[str]) -> TargetDepends:
    ts = []
    fs = []
    for d in depends:
        if d.startswith('f:'):
            fs.extend(glob.glob(d[2:]))
        else:
            ts.append(d)
    return TargetDepends(targets=ts, files=fs)


def _parse_creates(creates: TargetCreateType) -> TargetCreates:
    if callable(creates):
        return TargetCreates(func=creates, files=None)

    if isinstance(creates, list):
        files = []
        for f in creates:
            g = glob.glob(f)
            files.extend(g if g else [f])  # the might exist in the future
        return TargetCreates(files=files, func=None)


def _gen_create_files(func: Callable[[str], str] | None, depends: list[str]) -> list[str]:
    # if a function was provided to crate instead of list of file globs then
    # the file name are generated by call the provided function on all the dependencies
    if func:
        return list(map(func, depends))
    return []


def target(creates: TargetCreateType = [], depends: list[str] = []) -> Callable[[TargetFuncType], WrapTargetFuncType]:
    def target_dec(fn: TargetFuncType) -> WrapTargetFuncType:
        _targets[fn.__name__] = TargetData(name=fn.__name__,
                                           creates=_parse_creates(creates), depends=_parse_dependencies(depends))

        if _targets[fn.__name__].creates.func is not None:
            # gen the file names that target might create
            _targets[fn.__name__].creates.files = _gen_create_files(
                func=_targets[fn.__name__].creates.func,
                depends=_targets[fn.__name__].depends.files,
            )

        @wraps(fn)
        def target_wrap(opts: Namespace) -> None:
            fn(opts, _targets[fn.__name__])

        return target_wrap
    return target_dec


def _get_target_data(name: str) -> TargetData | None:
    return _targets.get(name, None)


def _check_target_exists(name: str) -> bool: return name in _targets


def _decide_target_exec(name: str) -> bool:
    # target executes if:
    # creates.files is empty
    # if ones of the fils in create does not exists
    # max mtimes (creates) < min mtimes (depends)
    t_data = _get_target_data(name)
    if not t_data:
        return False

    if not t_data.creates.files or not t_data.depends.files:
        return True

    n_creates = [Path(i) for i in t_data.creates.files]
    n_depends = [Path(i) for i in t_data.depends.files]

    # check whether file exists
    if not all(i.exists() for i in n_creates):
        return True

    if max(i.stat().st_mtime for i in n_creates) < max(i.stat().st_mtime for i in n_depends):
        return True

    return False


def _exec_target(name: str, opts: Namespace, target_globals: dict[str, Any]) -> None:
    if name not in _targets:
        print(f'unknown target: {name}', file=sys.stderr)
        exit(1)

    if callable(fn := target_globals[name]):
        t_data = _get_target_data(name)
        if t_data:
            for d in t_data.depends.targets:
                if d not in _targets:
                    print(f'unknown target: {d}', file=sys.stderr)
                    exit(1)
                # if _decide_target_exec(d):
                _exec_target(d, opts, target_globals)

        if _decide_target_exec(name):
            fn(opts)
