import re
from typing import Union, Tuple, Set, List
from argparse import ArgumentParser

from the_conf.utils import Index, TYPE_MAPPING

CONFIG_OPT_DEST = "config_file_path"
PASSKEY_OPT_DEST = "passkey"
OPTS_TYPE = Union[Tuple[str, ...], List[str], Set[str]]


def path_to_cmd_opt(path):
    return "--" + "-".join(map(str.lower, path))


def path_to_dest(path):
    return "_".join(path)


def _is_simple_list_path(path):
    """True if path is a simple list: ['name', Index]."""
    return len(path) >= 2 and path[-1] is Index


def _is_complex_list_path(path):
    """True if path is a complex list field."""
    return Index in path and path[-1] is not Index


def _resolve_type(param):
    """Resolve type from param, handling string type names."""
    raw = param.get("type")
    if raw is None:
        return None
    return TYPE_MAPPING.get(raw, raw)


def _iter_on_cmd_from_path(path, opts):
    """Extract complex list values from cmd line opts.

    Similar to environement.iter_on_environ_from_path but
    for command line arguments.

    >>> path = ['dictlist', Index, 'myint']
    >>> opts = ['--dictlist-0-myint', '1',
    ...         '--dictlist-1-myint', '2']
    >>> list(_iter_on_cmd_from_path(path, opts))
    [(['dictlist', 0, 'myint'], '1'),
     (['dictlist', 1, 'myint'], '2')]
    """
    if opts is None:
        return
    patterns = []
    indexes_places = []
    for i, elem in enumerate(path):
        if elem is Index:
            patterns.append(r"(\d+)")
            indexes_places.append(i)
        else:
            patterns.append(re.escape(elem.lower()))
    flag_pattern = "--" + "-".join(patterns)

    i = 0
    while i < len(opts):
        arg = opts[i]
        # Handle --flag=value and --flag value
        if "=" in arg:
            flag_part, value = arg.split("=", 1)
        else:
            flag_part = arg
            value = None

        match = re.fullmatch(flag_pattern, flag_part)
        if match:
            if value is None and i + 1 < len(opts):
                value = opts[i + 1]
                i += 1
            if value is not None:
                amended_path = [
                    (
                        int(match.group(indexes_places.index(j) + 1))
                        if j in indexes_places
                        else elem
                    )
                    for j, elem in enumerate(path)
                ]
                yield amended_path, value
        i += 1


def get_parser(
    path_n_params,
    config_file_cmd_line: OPTS_TYPE,
    passkey_cmd_line: OPTS_TYPE,
):
    parser = ArgumentParser()
    parser.add_argument(
        *config_file_cmd_line,
        dest=CONFIG_OPT_DEST,
        help="set main conf file to load configuration from",
    )
    parser.add_argument(
        *passkey_cmd_line,
        dest=PASSKEY_OPT_DEST,
        help="set main conf file to load configuration from",
    )
    seen_simple_lists = set()
    for path, _, param in path_n_params:
        parser_kw = {}

        if param.get("no_cmd"):
            continue

        # Complex list fields: skip argparse, handled manually
        if _is_complex_list_path(path):
            continue

        # Simple list: register with action='append'
        if _is_simple_list_path(path):
            list_path = path[:-1]
            list_key = tuple(list_path)
            if list_key in seen_simple_lists:
                continue
            seen_simple_lists.add(list_key)
            flag = param.get("cmd_line_opt") or path_to_cmd_opt(list_path)
            elem_type = _resolve_type(param)
            if elem_type and elem_type is not bool:
                parser_kw["type"] = elem_type
            if "among" in param:
                parser_kw["choices"] = param["among"]
            if "help_txt" in param:
                parser_kw["help"] = param["help_txt"]
            parser.add_argument(
                flag,
                dest=path_to_dest(list_path),
                action="append",
                **parser_kw,
            )
            continue

        # Regular (non-list) option
        flag = (
            param["cmd_line_opt"]
            if param.get("cmd_line_opt")
            else path_to_cmd_opt(path)
        )

        if "type" in param:
            parser_kw["type"] = param["type"]
            if param["type"] is bool and param.get("default") is False:
                param["action"] = "store_true"
                param["default"] = False
            elif param["type"] is bool and param.get("default") is True:
                param["action"] = "store_false"
                param["default"] = True
        if "among" in param:
            parser_kw["choices"] = param["among"]
        if "help_txt" in param:
            parser_kw["help"] = param["help_txt"]

        parser.add_argument(flag, dest=path_to_dest(path), **parser_kw)
    return parser


def yield_values_from_cmd(
    path_val_params,
    opts,
    config_file_cmd_line: OPTS_TYPE,
    passkey_cmd_line: OPTS_TYPE,
):
    parser = get_parser(
        path_val_params, config_file_cmd_line, passkey_cmd_line
    )
    cmd_line_args, remaining = parser.parse_known_args(opts)
    yield getattr(cmd_line_args, CONFIG_OPT_DEST)
    yield getattr(cmd_line_args, PASSKEY_OPT_DEST)

    seen_simple_lists = set()
    for path, _, param in path_val_params:
        if param.get("no_cmd"):
            continue

        # Complex list: parse from remaining args
        if _is_complex_list_path(path):
            yield from _iter_on_cmd_from_path(path, remaining)
            continue

        # Simple list: extract appended values
        if _is_simple_list_path(path):
            list_path = path[:-1]
            list_key = tuple(list_path)
            if list_key in seen_simple_lists:
                continue
            seen_simple_lists.add(list_key)
            values = getattr(
                cmd_line_args,
                path_to_dest(list_path),
                None,
            )
            if values:
                for i, v in enumerate(values):
                    yield list_path + [i], v
            continue

        # Regular option
        value = getattr(cmd_line_args, path_to_dest(path))
        if value is not None:
            yield path, value
