"""
Plugin handler.
"""

import glob
import importlib
import os
import re
import sys
from collections import defaultdict
from logging import DEBUG, INFO, basicConfig, getLogger
from textwrap import fill
from typing import Any, Dict, List, Sequence, Tuple, Union

# import pkg_resources as pr

if sys.version_info < (3, 10):
    from importlib_metadata import entry_points
else:
    from importlib.metadata import entry_points


def _normalize_unitcell_options(
    options: Sequence[Union[Tuple[str, str], Dict[str, Any]]],
) -> List[Dict[str, Any]]:
    """(name, help) or dict のリストを {name, help, required?, example?} のリストに統一。"""
    result = []
    for opt in options:
        if isinstance(opt, dict):
            result.append({
                "name": str(opt["name"]),
                "help": str(opt.get("help", opt.get("brief", ""))),
                "required": bool(opt.get("required", False)),
                "example": opt.get("example"),
            })
        else:
            name, help_ = opt[0], opt[1]
            required = opt[2] if len(opt) > 2 else False
            example = opt[3] if len(opt) > 3 else None
            result.append({
                "name": str(name),
                "help": str(help_),
                "required": bool(required),
                "example": example,
            })
    return result


def format_unitcell_usage(
    unitcell_name: str, options: Sequence[Union[Tuple[str, str], Dict[str, Any]]]
) -> Dict[str, str]:
    """
    options 構造体から CLI / API / YAML の 3 表記を生成する。
    Returns:
        {"cli": "...", "api": "...", "yaml": "..."}
    """
    opts = _normalize_unitcell_options(options)
    cli_parts = [f"genice3 {unitcell_name}"]
    api_args = []
    yaml_lines = [f"unitcell:", f"  name: {unitcell_name}"]
    for o in opts:
        ex = o.get("example")
        ex_str = str(ex) if ex is not None else "VALUE"
        cli_parts.append(f"--{o['name']} {ex_str}")
        if o.get("required"):
            cli_parts.append("(required)")
        api_args.append(f"{o['name']}={repr(ex)}" if ex is not None else f"{o['name']}=None")
        yaml_lines.append(f"  {o['name']}: {ex}" if ex is not None else f"  {o['name']}: ...")
    return {
        "cli": " ".join(cli_parts) + "\n  " + "\n  ".join(f"--{o['name']}: {o['help']}" for o in opts),
        "api": f'UnitCell("{unitcell_name}", ' + ", ".join(api_args) + ")",
        "yaml": "\n".join(yaml_lines),
    }


def _is_water_module(module, category):
    """Return True if the module is a water model (for molecule plugins)."""
    if category != "molecule":
        return False
    if "water" in module.__dict__:
        return True
    if hasattr(module, "Molecule"):
        try:
            m = module.Molecule()
            return getattr(m, "is_water", False)
        except Exception:
            pass
    return False


def scan(category):
    """
    Scan available plugins.
    """
    logger = getLogger()

    modules = {}
    desc = dict()
    iswater = dict()
    refs = dict()
    tests = dict()

    logger.info(f"\nPredefined {category}")
    module = importlib.import_module(f"genice3.{category}")
    mods = []
    for path in module.__path__:
        for mod in sorted(glob.glob(path + "/*.py")):
            mod = os.path.basename(mod)[:-3]
            if mod[:2] != "__":
                mods.append(mod)
    logger.info(mods)
    modules["system"] = mods

    for mod in modules["system"]:
        try:
            module = importlib.import_module(f"genice3.{category}.{mod}")
            if "desc" in module.__dict__:
                desc[mod] = module.desc["brief"]
                if "ref" in module.desc:
                    refs[mod] = module.desc["ref"]
                if "test" in module.desc:
                    tests[mod] = module.desc["test"]
            iswater[mod] = _is_water_module(module, category)
        except BaseException:
            pass

    logger.info(f"Extra {category}")
    groupname = f"genice3_{category}"
    mods = []
    # for ep in pr.iter_entry_points(group=groupname):
    for ep in entry_points(group=groupname):
        mods.append(ep.name)
        try:
            module = ep.load()
            if "desc" in module.__dict__:
                desc[ep.name] = module.desc["brief"]
                if "ref" in module.desc:
                    refs[ep.name] = module.desc["ref"]
                if "test" in module.desc:
                    tests[mod] = module.desc["test"]
            iswater[ep.name] = _is_water_module(module, category)
        except BaseException:
            pass
    logger.info(mods)
    modules["extra"] = mods

    logger.info(f"Local {category}")
    mods = [
        os.path.basename(mod)[:-3] for mod in sorted(glob.glob(f"./{category}/*.py"))
    ]
    logger.info(mods)
    for mod in mods:
        module = importlib.import_module(f"{category}.{mod}")
        if "desc" in module.__dict__:
            desc[mod] = module.desc["brief"]
            if "ref" in module.desc:
                refs[mod] = module.desc["ref"]
            if "test" in module.desc:
                tests[mod] = module.desc["test"]
        iswater[mod] = _is_water_module(module, category)
    logger.info(mods)
    modules["local"] = mods
    modules["desc"] = desc
    modules["iswater"] = iswater
    modules["refs"] = refs
    modules["tests"] = tests

    return modules


def descriptions(category, width=72, water=False, groups=("system", "extra", "local")):
    """
    Show the list of available plugins in the category.

    Options:
      width=72      Width of the output.
      water=False   Pick up water molecules only (for molecule plugin).
    """
    titles = {
        "lattice": {
            "system": "1. Lattice structures served with GenIce",
            "extra": "2. Lattice structures served by external plugins",
            "local": "3. Lattice structures served locally",
            "title": "[Available lattice structures]",
        },
        "format": {
            "system": "1. Formatters served with GenIce",
            "extra": "2. Formatters served by external plugins",
            "local": "3. Formatters served locally",
            "title": "[Available formatters]",
        },
        "loader": {
            "system": "1. File types served with GenIce",
            "extra": "2. File types served by external eplugins",
            "local": "3. File types served locally",
            "title": "[Available input file types]",
        },
        "molecule": {
            "system": "1. Molecules served with GenIce",
            "extra": "2. Molecules served by external plugins",
            "local": "3. Molecules served locally",
            "title": "[Available molecules]",
        },
    }
    mods = scan(category)
    catalog = f" \n \n{titles[category]['title']}\n \n"
    desc = mods["desc"]
    iswater = mods["iswater"]
    for group in groups:
        desced = defaultdict(list)
        undesc = []
        for L in mods[group]:
            if category == "molecule":
                if L not in iswater:
                    iswater[L] = False
                if water and not iswater[L]:
                    continue
                if not water and iswater[L]:
                    continue
            if L in desc:
                desced[desc[L]].append(L)
            else:
                undesc.append(L)
        for dd in desced:
            desced[dd] = ", ".join(desced[dd])
        catalog += f"{titles[category][group]}\n \n"
        table = ""
        for dd in sorted(desced, key=lambda x: desced[x]):
            table += f"{desced[dd]}\t{dd}\n"
        if table == "":
            table = "(None)\n"
        table = [
            fill(
                line,
                width=width,
                drop_whitespace=False,
                expand_tabs=True,
                tabsize=16,
                subsequent_indent=" " * 16,
            )
            for line in table.splitlines()
        ]
        table = "\n".join(table) + "\n"
        undesc = " ".join(undesc)
        if undesc != "":
            undesc = "(Undocumented) " + undesc
        catalog += table + "----\n" + undesc + "\n \n \n"
    return catalog


def plugin_descriptors(category, water=False, groups=("system", "extra", "local")):
    """
    Show the list of available plugins in the category.

    Options:
      water=False   Pick up water molecules only (for molecule plugin).
    """
    mods = scan(category)
    catalog = dict()
    desc = mods["desc"]
    iswater = mods["iswater"]
    refs = mods["refs"]
    for group in groups:
        desced = defaultdict(list)
        undesc = []
        refss = defaultdict(set)
        for L in mods[group]:
            if category == "molecule":
                if L not in iswater:
                    iswater[L] = False
                if water and not iswater[L]:
                    continue
                if not water and iswater[L]:
                    continue
            if L in desc:
                # desc[L] is the brief description of the module
                # L is the name of module (name of ice)
                desced[desc[L]].append(L)
                if L in refs:
                    refss[desc[L]] |= set([label for key, label in refs[L].items()])
            else:
                undesc.append(L)
        catalog[group] = [desced, undesc, refss]
    return catalog


def audit_name(name: str, category: str = "plugin") -> str:
    """
    Audit the mol name to avoid the access to unexpected files
    """
    match = re.match("^[A-Za-z0-9-_]+$", name)
    if match is not None:
        return name
    match = re.match(r"^\[([A-Za-z0-9-_]+) .*\]$", name)
    if match is not None:
        return match.group(1)
    raise ValueError(f"Dubious {category} name: {name}")


def import_extra(category, name):
    logger = getLogger()
    logger.info(f"Extra {category} plugin: {name}")
    groupname = f"genice3_{category}"
    module = None
    # for ep in pr.iter_entry_points(group=groupname):
    for ep in entry_points(group=groupname):
        logger.debug(f"    Entry point: {ep}")
        if ep.name == name:
            logger.debug(f"      Loading {name}...")
            module = ep.load()
    if module is None:
        raise ImportError(f"Nonexistent or failed to load the {category} module: {name}")
    return module


def safe_import(category, name):
    """
    Load a plugin.

    The plugins can exist either in the system, as a extra plugin, or in the
    local folder.

    category: The type of the plugin; "lattice", "format", "molecule", or "loader".
    name:     The name of the plugin.
    """
    logger = getLogger()
    if category not in ("exporter", "molecule", "unitcell", "group"):
        raise ValueError(
            f"category must be 'exporter', 'molecule', 'unitcell', or 'group', got: {category}"
        )

    # single ? as a plugin name ==> show descriptions (list all)
    if name == "?":
        print(descriptions(category))
        sys.exit(0)

    # SYMBOL? ==> show usage for that plugin (then exit)
    usage = False
    if len(name) > 1 and name[-1] == "?":
        usage = True
        name = name[:-1]

    module_name = audit_name(name, category)

    module = None
    fullname = f"{category}.{module_name}"
    logger.debug(f"Try to Load a local module: {fullname}")
    # まずカレントディレクトリを path に挿入して試す（実行ディレクトリの group/ 等を読むため）
    cwd = os.getcwd()
    path_inserted = cwd not in sys.path
    if path_inserted:
        sys.path.insert(0, cwd)
    try:
        try:
            module = importlib.import_module(fullname)
            logger.debug("Succeeded (local from cwd or path).")
        except ModuleNotFoundError:
            logger.debug(f"Module not found: {fullname}")
            module = None
        except ImportError as e:
            logger.error(f"Error importing module {fullname}: {str(e)}")
            raise
    finally:
        if path_inserted and sys.path and sys.path[0] == cwd:
            sys.path.pop(0)
    if module is None:
        fullname = f"genice3.{category}.{module_name}"
        logger.debug(f"Try to load a system module: {fullname}")
        try:
            module = importlib.import_module(fullname)
            logger.debug("Succeeded.")
        except ModuleNotFoundError:
            logger.debug(f"Module not found: {fullname}")
            module = None
        except ImportError as e:
            logger.error(f"Error importing module {fullname}: {str(e)}")
            raise
    if module is None:
        logger.debug(f"Try to load an extra module: {fullname}")
        module = import_extra(category, module_name)
        logger.debug("Succeeded.")

    if usage:
        if "desc" in module.__dict__:
            d = module.desc
            logger.info(f"Usage for '{name}' plugin")
            if category == "unitcell" and d.get("options"):
                u = format_unitcell_usage(name, d["options"])
                print("CLI:  ", u["cli"])
                print("API:  ", u["api"])
                print("YAML:\n", u["yaml"])
            elif d.get("usage"):
                print(d["usage"])
            else:
                print("(no usage)")
            sys.exit(0)

    return module


def UnitCell(name, **kwargs):
    """
    Shortcut for safe_import.
    """
    return safe_import("unitcell", name).UnitCell(**kwargs)


def Molecule(name, **kwargs):
    """
    Shortcut for safe_import.
    """
    return safe_import("molecule", name).Molecule(**kwargs)


def Exporter(name, **kwargs):
    """
    Shortcut for safe_import.
    """
    return safe_import("exporter", name)


def Group(name, **kwargs):
    """
    Shortcut for safe_import.
    """
    return safe_import("group", name).Group(**kwargs)


def get_exporter_format_rows(category="exporter", groups=("system", "extra", "local")):
    """
    Collect format_desc from all exporter plugins and return rows for the README table.

    Each exporter module may define a ``format_desc`` dict with keys:
      aliases: list of option names (e.g. ["g", "gromacs"])
      application: str (markdown allowed)
      extension: str (e.g. ".gro")
      water: str (e.g. "Atomic positions")
      solute: str
      hb: str (e.g. "none", "o", "auto")
      remarks: str
      suboptions: str (optional; short description of :key value options, e.g. "water_model: 3site, 4site, 6site, tip4p")

    Returns a list of dicts with keys name, application, extension, water, solute, hb, remarks, suboptions.
    """
    logger = getLogger()
    mods = scan(category)
    rows = []
    seen = set()

    for group in groups:
        for name in mods.get(group, []):
            if name in seen:
                continue
            try:
                if group == "system":
                    mod = importlib.import_module(f"genice3.{category}.{name}")
                elif group == "extra":
                    mod = None
                    for ep in entry_points(group=f"genice3_{category}"):
                        if ep.name == name:
                            mod = ep.load()
                            break
                    if mod is None:
                        continue
                else:
                    try:
                        mod = importlib.import_module(f"{category}.{name}")
                    except ModuleNotFoundError:
                        continue
                if not hasattr(mod, "format_desc"):
                    continue
                fd = mod.format_desc
                aliases = fd.get("aliases", [name])
                name_col = ", ".join(f"`{a}`" for a in aliases)
                rows.append(
                    {
                        "name": name_col,
                        "application": fd.get("application", ""),
                        "extension": fd.get("extension", ""),
                        "water": fd.get("water", ""),
                        "solute": fd.get("solute", ""),
                        "hb": fd.get("hb", ""),
                        "remarks": fd.get("remarks", ""),
                        "suboptions": fd.get("suboptions", ""),
                    }
                )
                seen.add(name)
            except Exception as e:
                logger.debug(f"Skip {name} for format table: {e}")
    return rows


if __name__ == "__main__":
    basicConfig(level=INFO)
    if len(sys.argv) == 1:
        cats = ("lattice", "format", "molecule", "loader")
    else:
        cats = sys.argv[1:]
    modules = {cat: scan(cat) for cat in cats}
    print(modules)
