from __future__ import annotations

import re
from collections import defaultdict
from collections.abc import Iterable, Mapping, Sequence
from functools import cache
from inspect import Parameter
from itertools import takewhile
from logging import getLogger
from os import PathLike
from pathlib import Path
from typing import Any, is_typeddict

from vapoursynth import Error, core

from .constants import (
    _ATTR_IMPL_END,
    _ATTR_IMPL_START,
    _CORE_IMPL_END,
    _CORE_IMPL_START,
    _IMPL_END,
    _IMPL_START,
    _PLUGINS_IMPL_END,
    _PLUGINS_IMPL_START,
    _callback_signatures,
    _wrappers,
)
from .types import (
    FunctionInterface,
    Implementation,
    PluginInterface,
    VideoNodeType,
    WrappedFunction,
    _CoreLike,
    parse_type,
)
from .utils import _get_cores, _get_dir, _get_plugins, _get_typed_dict_repr, _replace_known_callback_signature

log = getLogger(__name__)


def load_plugins(paths: Iterable[str | PathLike[str]]) -> set[str]:
    """
    Load the plugins from a list of dll or path folders.

    Returns the new loaded namespace plugins.
    """
    old_plugins = {p.namespace for p in core.plugins()}

    for path in paths:
        path = Path(path)

        if not path.exists():
            raise ValueError(f'This path "{path}" doesn\'t exist.')

        if path.is_dir():
            core.std.LoadAllPlugins(str(path))
        else:
            # std.LoadAllPlugins silently skips the plugins that fail to load.
            try:
                core.std.LoadPlugin(str(path))
            except Error as e:
                log.exception(e)

    return {p.namespace for p in _get_plugins()} - old_plugins


def retrieve_plugins(core_like: Sequence[_CoreLike]) -> Sequence[PluginInterface]:
    """
    Get a sequence of PluginInferface.

    A PluginInferface offers an interface for each core-like it owns with its functions attached to it.
    """
    plugins = list[PluginInterface]()

    for plugin in _get_plugins():
        functions = defaultdict[str, list[FunctionInterface]](list)

        for cl in core_like:
            # Some plugins only have vs.Core as bound core
            if plugin.namespace not in _get_dir(cl):
                continue

            # Get the actual plugin attached to its core to get the right functions signatures.
            if cl is not core.core:
                plugin = getattr(cl, plugin.namespace)

            functions[cl.__class__.__name__].extend(
                # Only gets the functions that __dir__ provides
                FunctionInterface(f.name, f.__signature__)
                for f in plugin.functions()
                if f.name in _get_dir(plugin)
            )

        plugins.append(PluginInterface(plugin.namespace, functions, plugin.name))

    return plugins


def construct_implementation(interface: PluginInterface) -> Implementation:
    """Contructs a full implementation block with all the functions for all the cores-like."""

    functions_map = dict[str, list[WrappedFunction]]()
    extras = list[str]()

    for core_name, functions in interface.functions.items():
        functions_list = list[WrappedFunction]()

        for function in functions:
            parameters = function.signature.parameters.copy()

            for k, v in parameters.items():
                parameters[k] = v.replace(annotation=parse_type(v.annotation))

            # Replaces the anonymous callback types to known signatures.
            if param_names := _callback_signatures.get(interface.namespace, {}).get(function.name):
                for name in param_names:
                    parameters[name] = _replace_known_callback_signature(parameters[name], interface, function)

            # If a function returns Any, it's probably a APIv3 plugin.
            # We assume it always returns a VideoNode but we know it's not always the case...
            if function.signature.return_annotation == Any:
                return_annotation = VideoNodeType()
                log.debug("APIv3 plugin detected: '%s.%s.%s'", core_name, interface.namespace, function.name)
            else:
                return_annotation = parse_type(function.signature.return_annotation, True)

            signature = function.signature.replace(
                parameters=(Parameter("self", Parameter.POSITIONAL_OR_KEYWORD), *parameters.values()),
                return_annotation=return_annotation,
            )

            wrapper = (
                f"_Wrapper_{core_name}_bound_{function.name}.Function"
                if function.name in _wrappers.get(core_name, set())
                else None
            )

            functions_list.append(WrappedFunction(f"{function.name}{signature}: ...", wrapper))

            if is_typeddict(td := signature.return_annotation):
                extras.append(_get_typed_dict_repr(td))

        functions_map[core_name] = functions_list

    return Implementation(interface.namespace, functions_map, interface.description, extras)


def get_implementations_from_input(text: str) -> list[Implementation]:
    """Parse a file to extract plugin implementations."""

    plugins_impl_block_regex = rf"{_PLUGINS_IMPL_START}(.*?){_PLUGINS_IMPL_END}"

    plugins_impl_block_matched = re.search(plugins_impl_block_regex, text, re.DOTALL)

    if not plugins_impl_block_matched:
        return []

    # Regex to capture class blocks like "_Core_bound", "_VideoNode_bound" or "_AudioNode_bound"
    plugins_impl_block_pattern_text = r"class _(\w+)_bound:\s*class Plugin.*?:([\s\S]*?)(?=\n\s*class _\w+_bound:|\Z)"
    plugins_impl_block_pattern = re.compile(plugins_impl_block_pattern_text, re.MULTILINE)

    # Regex to capture decorator + full function definition lines
    func_pattern_text = r"^\s*(?:@([^\n]*))?\s*\n?\s*def ([\s\S]+?-> [\w\[\], |]+: \.\.\.)"
    func_pattern = re.compile(func_pattern_text, re.MULTILINE)

    implementations = list[Implementation]()

    # Regex to capture <implementation/{plugin_name}>
    impl_capture_regex = rf"{_IMPL_START.format(name='([^>]+)')}(.*?){_IMPL_END.format(name='\\1')}"

    for name, body in re.findall(impl_capture_regex, plugins_impl_block_matched.group(), re.DOTALL):
        if not body:
            raise ValueError(f"No plugin implementation block found for {name}.")

        extras = list(takewhile(lambda s: not s.startswith("class"), (s for s in body.splitlines() if s)))

        functions = defaultdict[str, list[WrappedFunction]](list)

        for core_like, fbody in plugins_impl_block_pattern.findall(body):
            for decorator, func_def in func_pattern.findall(fbody):
                # Normalize the function definition into one stripped line
                normalized_func = " ".join(func_def.split()).replace("( ", "(").replace(", )", ")")

                # Store functions under the core_like key
                functions[core_like].append(WrappedFunction(normalized_func, decorator))

        doc = _extract_description(text, name, functions)

        implementations.append(Implementation(name, functions, doc, extras))

    return implementations


@cache
def _search_core_impl(core_name: str, text: str) -> re.Match[str] | None:
    """Regex to capture the whole plugins bound block"""
    return re.search(rf"{_CORE_IMPL_START}(.*?){_CORE_IMPL_END}".format(core_name=core_name), text, re.DOTALL)


def _extract_description(text: str, ns: str, functions: Mapping[str, Sequence[WrappedFunction]]) -> str:
    core_name = next(iter(functions))

    core_impl_matched = _search_core_impl(core_name, text)

    if core_impl_matched is None:
        raise ValueError(f"No core implementation block found for {core_name}.")

    # Regex to capture the attribute block
    attr_matched = re.search(
        rf"{_ATTR_IMPL_START}(.*?){_ATTR_IMPL_END}".format(core_name=core_name, name=ns),
        core_impl_matched.group(0),
        re.DOTALL,
    )
    if not attr_matched:
        raise ValueError(f"No attribute block found for {core_name}.")

    doc_matched = re.search(r"(?:\"\"\"(.*?)\"\"\"|)$", attr_matched.group(1), re.DOTALL)

    return "" if not doc_matched else doc_matched.group(1)


def write_implementations(implementations: list[Implementation], template: str) -> str:
    """Replace the plugin implementations block in `template` with the given implementations."""
    body = "\n".join(impl.as_stub() for impl in sorted(implementations))
    replacement = f"{_PLUGINS_IMPL_START}\n{body}\n{_PLUGINS_IMPL_END}"

    return re.sub(
        rf"{_PLUGINS_IMPL_START}.*{_PLUGINS_IMPL_END}",
        replacement,
        template,
        flags=re.DOTALL,
    )


def write_plugins_bound(implementations: list[Implementation], template: str) -> str:
    """Replace the plugin bound blocks in `template` with the given implementations."""
    for core_like in _get_cores():
        cname = core_like.__class__.__name__

        plugins_bound = "\n".join(
            (
                f"{_ATTR_IMPL_START.format(core_name=cname, name=i.namespace)}\n"
                f"    {i.namespace}: Final[_{i.namespace}._{cname}_bound.Plugin]\n"
                f'    """{i.description}"""\n'
                f"{_ATTR_IMPL_END.format(core_name=cname, name=i.namespace)}"
            )
            for i in sorted(implementations, key=lambda i: i.namespace)
            if cname in i.functions
        )

        template = re.sub(
            rf"{_CORE_IMPL_START}.*{_CORE_IMPL_END}".format(core_name=cname),
            rf"{_CORE_IMPL_START}\n{plugins_bound}\n{_CORE_IMPL_END}".format(core_name=cname),
            template,
            flags=re.DOTALL,
        )

    return template
