#!/usr/bin/env python3
import argparse
import configparser
import importlib
import pkgutil
from typing import Dict, ItemsView, List, Literal, Tuple, Union, cast

from typing_extensions import NotRequired, TypedDict


AlertingLevel = Literal["error", "warn", "none"]
ValueType = Union[AlertingLevel, str, List[str]]


class Contract(TypedDict):
    name: str
    type: Literal["layers", "independence", "forbidden"]
    layers: NotRequired[List[str]]
    modules: NotRequired[List[str]]
    ignore_imports: NotRequired[List[str]]
    unmatched_ignore_imports_alerting: NotRequired[AlertingLevel]
    source_modules: NotRequired[List[str]]
    forbidden_modules: NotRequired[List[str]]


NOTICE = (
    "# This file has been generated by ./scripts/generate-import-linter-config.py,"
    " do not edit by hand!"
)
CONTRACTS = "importlinter:contracts"
STATIC_CONFIG = {
    "importlinter": {
        "root_package": "ggshield",
        "include_external_packages": True,
    },
    CONTRACTS: [
        {
            "name": "ggshield-layers",
            "type": "layers",
            "layers": [
                "ggshield.__main__",
                "ggshield.cmd|%",
                "ggshield.verticals|%",
                "ggshield.core",
                "click | ggshield.utils | pygitguardian",
            ],
            "ignore_imports": [
                "ggshield.cmd.** -> ggshield.cmd.utils.*",
                "ggshield.utils.click.** -> click",
            ],
            "unmatched_ignore_imports_alerting": "warn",
        },
        {
            "name": "verticals-cmd-transversals",
            "type": "forbidden",
            "source_modules": [
                "ggshield.cmd.%",
            ],
            "forbidden_modules": [
                "ggshield.verticals.%",
            ],
            "ignore_imports": [
                "ggshield.cmd.{}.** -> ggshield.verticals.{}",
                "ggshield.cmd.{}.** -> ggshield.verticals.{}.**",
                # FIXME: #521 - enforce boundaries between cmd.auth and verticals.hmsl
                "ggshield.cmd.auth.** -> ggshield.verticals.hmsl.**",
                # Logic to install hooks for AI assistants
                "ggshield.cmd.install -> ggshield.verticals.secret.ai_hook",
            ],
            "unmatched_ignore_imports_alerting": "none",
        },
    ],
}


def get_submodules(*, name: str, prefixed: bool) -> List[str]:
    """Retrieve the modules included in a package"""
    module = importlib.import_module(name)
    return [
        x.name
        for x in pkgutil.iter_modules(
            path=module.__path__,
            prefix=f"{module.__name__}." if prefixed else "",
        )
    ]


def expand_glob(line: str) -> List[str]:
    """
    Expand the *glob*

    supported globs:
    - "xxx.%" => ["xxx.sub_module_a", "xxx.sub_module_b"]
    - "xxx|%" => ["xxx.sub_module_a | xxx.sub_module_b"]
    - "xxx.{} -> yyy.{}" => [
        "xxx.sub_module_a -> yyy.sub_module_a",
        "xxx.sub_module_b -> yyy.sub_module_b"
      ]
    - "xxx" (no globs) => ["xxx"]
    """
    if line.endswith(".%"):
        return sorted(get_submodules(name=line[:-2], prefixed=True))
    if line.endswith("|%"):
        return [" | ".join(sorted(get_submodules(name=line[:-2], prefixed=True)))]
    if "{}" in line:
        name = line[: line.index(".{}")]
        expanded_lines = []
        for module in get_submodules(name=name, prefixed=False):
            expanded_line = line.replace("{}", module)
            if module == "plugin":
                expanded_line = expanded_line.replace(
                    "ggshield.verticals.plugin", "ggshield.core.plugin"
                )
            expanded_lines.append(expanded_line)
        return expanded_lines
    return [line]


def expand_modules(
    *,
    values: List[str],
    ordered: bool = False,
) -> List[str]:
    """Build the list with the glob expanded"""
    expanded = (
        module_name
        for module_name_or_glob in values
        for module_name in expand_glob(module_name_or_glob)
    )
    return sorted(expanded) if ordered else list(expanded)


def expand_value(value: ValueType, key: str) -> ValueType:
    """Build the list with items expanded"""
    if isinstance(value, list):
        typed_value = cast(List[str], value)
        return expand_modules(
            values=typed_value,
            ordered=key != "layers",
        )

    return value


def normalize_value(data: ValueType) -> Union[bool, str]:
    """Normalize value to be compatible with import-linter config format"""
    if isinstance(data, list):
        return "\n".join(["", *data])
    return data


def normalize_contract(contract: Contract) -> Tuple[str, Dict[str, Union[bool, str]]]:
    """Normalize contract to be compatible with import-linter config format"""
    cid = compute_contract_id(contract["name"])
    content = {
        key: normalize_value(expand_value(value, key))
        for key, value in cast(ItemsView[str, ValueType], contract.items())
    }
    return cid, content


def compute_contract_id(name: str) -> str:
    """Compute an ID for the contract based on its name"""
    slug = name.lower().replace(" ", "-")
    return f"importlinter:contract:{slug}"


def normalize_contracts(config) -> Dict[str, Dict[str, Union[bool, str]]]:
    """Build contracts from template and expand the globs"""
    normalized = {key: value for key, value in config.items() if key != CONTRACTS}

    for contract in config[CONTRACTS]:
        cid, content = normalize_contract(contract)
        normalized[cid] = content

    return normalized


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("output", type=argparse.FileType("w"), nargs="?", default="-")
    args = parser.parse_args()

    config = configparser.ConfigParser()
    config.read_dict(normalize_contracts(STATIC_CONFIG))
    args.output.write(f"{NOTICE}\n\n")
    config.write(args.output)


if __name__ == "__main__":
    main()
