"""
TODO:
 - -x / --exec - os.exec entrypoint
  - refuse to install non-exec-relevant Bootstraps when chosen
"""
# ruff: noqa: UP006 UP007
import argparse
import dataclasses as dc
import io
import itertools
import sys
import typing as ta

from .. import lang
from .base import Bootstrap
from .harness import BOOTSTRAP_TYPES_BY_NAME
from .harness import bootstrap


if ta.TYPE_CHECKING:
    import runpy

else:
    runpy = lang.proxy_import('runpy')


##


class _OrderedArgsAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        if 'ordered_args' not in namespace:
            setattr(namespace, 'ordered_args', [])
        if self.const is not None:
            value = self.const
        else:
            value = values
        namespace.ordered_args.append((self.dest, value))


def _or_opt(ty):
    return (ty, ta.Optional[ty])


def _int_or_str(v):
    try:
        return int(v)
    except ValueError:
        return v


def _add_arguments(parser: argparse.ArgumentParser) -> None:
    # ta.Optional[ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]]

    for cname, cls in BOOTSTRAP_TYPES_BY_NAME.items():
        for fld in dc.fields(cls.Config):
            aname = f'--{cname}:{fld.name}'
            kw: ta.Dict[str, ta.Any] = {}

            if fld.type in _or_opt(str):
                pass
            elif fld.type in _or_opt(bool):
                kw.update(const=True, nargs=0)
            elif fld.type in _or_opt(int):
                kw.update(type=int)
            elif fld.type in _or_opt(float):
                kw.update(type=float)
            elif fld.type in _or_opt(ta.Union[int, str]):
                kw.update(type=_int_or_str)

            elif fld.type in (
                    *_or_opt(ta.Sequence[str]),
                    *_or_opt(ta.Mapping[str, ta.Optional[str]]),
                    *_or_opt(ta.Mapping[int, ta.Union[int, str, None]]),
                    *_or_opt(ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]),
            ):
                if aname[-1] != 's':
                    raise NameError(aname)
                aname = aname[:-1]

            else:
                raise TypeError(fld)

            parser.add_argument(aname, action=_OrderedArgsAction, **kw)


def _process_arguments(args: ta.Any) -> ta.Sequence[Bootstrap.Config]:
    if not (oa := getattr(args, 'ordered_args', None)):
        return []

    cfgs: ta.List[Bootstrap.Config] = []

    for cname, cargs in [
        (n, list(g))
        for n, g in
        itertools.groupby(oa, key=lambda s: s[0].partition(':')[0])
    ]:
        ccls = BOOTSTRAP_TYPES_BY_NAME[cname].Config
        flds = {f.name: f for f in dc.fields(ccls)}

        kw: ta.Dict[str, ta.Any] = {}
        for aname, aval in cargs:
            k = aname.partition(':')[2]

            if k not in flds:
                k += 's'
                fld = flds[k]

                if fld.type in _or_opt(ta.Sequence[str]):
                    kw.setdefault(k, []).append(aval)

                elif fld.type in _or_opt(ta.Mapping[str, ta.Optional[str]]):
                    if '=' not in aval:
                        kw.setdefault(k, {})[aval] = None
                    else:
                        ek, _, ev = aval.partition('=')
                        kw.setdefault(k, {})[ek] = ev

                elif fld.type in _or_opt(ta.Mapping[int, ta.Union[int, str, None]]):
                    fk, _, fv = aval.partition('=')
                    if not fv:
                        kw.setdefault(k, {})[int(fk)] = None
                    else:
                        kw.setdefault(k, {})[int(fk)] = _int_or_str(fv)

                elif fld.type in _or_opt(ta.Mapping[str, ta.Tuple[ta.Optional[int], ta.Optional[int]]]):
                    fk, _, fv = aval.partition('=')
                    if ',' in fv:
                        tl, tr = fv.split(',')
                    else:
                        tl, tr = None, None
                    kw.setdefault(k, {})[fk] = (_int_or_str(tl) if tl else None, _int_or_str(tr) if tr else None)

                else:
                    raise TypeError(fld)

            else:
                kw[k] = aval

        cfg = ccls(**kw)
        cfgs.append(cfg)

    return cfgs


##


def _main() -> int:
    parser = argparse.ArgumentParser()

    _add_arguments(parser)

    parser.add_argument('-m', '--module', action='store_true')
    parser.add_argument('target')
    parser.add_argument('args', nargs=argparse.REMAINDER)

    args = parser.parse_args()
    cfgs = _process_arguments(args)

    with bootstrap(*cfgs):
        tgt = args.target

        sys.argv = [tgt, *(args.args or ())]

        if args.module:
            runpy._run_module_as_main(tgt)  # type: ignore  # noqa

        else:
            with io.open_code(tgt) as fp:
                src = fp.read()

            ns = dict(
                __name__='__main__',
                __file__=tgt,
                __builtins__=__builtins__,
                __spec__=None,
            )

            import __main__  # noqa
            __main__.__dict__.clear()
            __main__.__dict__.update(ns)
            exec(compile(src, tgt, 'exec'), __main__.__dict__, __main__.__dict__)

        return 0


if __name__ == '__main__':
    sys.exit(_main())
