from typing import List, Any, Dict, Tuple, TYPE_CHECKING, cast

from debputy._manifest_constants import (
    ManifestVersion,
    MK_MANIFEST_VERSION,
    MK_INSTALLATIONS,
    SUPPORTED_MANIFEST_VERSIONS,
    MK_MANIFEST_DEFINITIONS,
    MK_PACKAGES,
    MK_MANIFEST_VARIABLES,
    MK_MANIFEST_REMOVE_DURING_CLEAN,
)
from debputy.exceptions import DebputySubstitutionError
from debputy.installations import InstallRule
from debputy.manifest_parser.base_types import FileSystemMatchRule
from debputy.manifest_parser.exceptions import ManifestParseException
from debputy.manifest_parser.parser_data import ParserContextData
from debputy.manifest_parser.tagging_types import DebputyParsedContent
from debputy.manifest_parser.util import AttributePath
from debputy.plugin.api.impl import DebputyPluginInitializerProvider
from debputy.plugin.api.parser_tables import (
    OPARSER_MANIFEST_ROOT,
    OPARSER_MANIFEST_DEFINITIONS,
    OPARSER_PACKAGES,
)
from debputy.plugin.api.spec import (
    not_integrations,
    INTEGRATION_MODE_DH_DEBPUTY_RRR,
    INTEGRATION_MODE_FULL,
    only_integrations,
)
from debputy.plugins.debputy.build_system_rules import register_build_system_rules
from debputy.substitution import VariableNameState, SUBST_VAR_RE

if TYPE_CHECKING:
    from debputy.highlevel_manifest_parser import YAMLManifestParser


def register_manifest_root_rules(api: DebputyPluginInitializerProvider) -> None:
    # Registration order matters. Notably, definitions must come before anything that can
    # use definitions (variables), which is why it is second only to the manifest version.
    api.pluggable_manifest_rule(
        OPARSER_MANIFEST_ROOT,
        MK_MANIFEST_VERSION,
        ManifestVersionFormat,
        _handle_version,
        source_format=ManifestVersion,
    )
    api.pluggable_object_parser(
        OPARSER_MANIFEST_ROOT,
        MK_MANIFEST_DEFINITIONS,
        object_parser_key=OPARSER_MANIFEST_DEFINITIONS,
        on_end_parse_step=lambda _a, _b, _c, mp: mp._ensure_package_states_is_initialized(),
    )
    api.pluggable_manifest_rule(
        OPARSER_MANIFEST_DEFINITIONS,
        MK_MANIFEST_VARIABLES,
        ManifestVariablesParsedFormat,
        _handle_manifest_variables,
        source_format=Dict[str, str],
    )
    api.pluggable_manifest_rule(
        OPARSER_MANIFEST_ROOT,
        MK_INSTALLATIONS,
        List[InstallRule],
        _handle_installation_rules,
        expected_debputy_integration_mode=not_integrations(
            INTEGRATION_MODE_DH_DEBPUTY_RRR
        ),
    )
    api.pluggable_manifest_rule(
        OPARSER_MANIFEST_ROOT,
        MK_MANIFEST_REMOVE_DURING_CLEAN,
        List[RemoveDuringCleanParsedFormat],
        _handle_remove_during_clean,
        expected_debputy_integration_mode=only_integrations(
            INTEGRATION_MODE_FULL,
        ),
        source_format=List[FileSystemMatchRule],
    )
    api.pluggable_object_parser(
        OPARSER_MANIFEST_ROOT,
        MK_PACKAGES,
        object_parser_key=OPARSER_PACKAGES,
        on_end_parse_step=lambda _a, _b, _c, mp: mp._ensure_package_states_is_initialized(),
        nested_in_package_context=True,
    )
    register_build_system_rules(api)


class ManifestVersionFormat(DebputyParsedContent):
    manifest_version: ManifestVersion


class ListOfInstallRulesFormat(DebputyParsedContent):
    elements: List[InstallRule]


class DictFormat(DebputyParsedContent):
    mapping: Dict[str, Any]


class ManifestVariablesParsedFormat(DebputyParsedContent):
    variables: Dict[str, str]


class RemoveDuringCleanParsedFormat(DebputyParsedContent):
    pattern: FileSystemMatchRule


def _handle_version(
    _name: str,
    parsed_data: ManifestVersionFormat,
    _attribute_path: AttributePath,
    _parser_context: ParserContextData,
) -> str:
    manifest_version = parsed_data["manifest_version"]
    if manifest_version not in SUPPORTED_MANIFEST_VERSIONS:
        raise ManifestParseException(
            "Unsupported manifest-version.  This implementation supports the following versions:"
            f' {", ".join(repr(v) for v in SUPPORTED_MANIFEST_VERSIONS)}"'
        )
    return manifest_version


def _handle_manifest_variables(
    _name: str,
    parsed_data: ManifestVariablesParsedFormat,
    variables_path: AttributePath,
    parser_context: ParserContextData,
) -> None:
    variables = parsed_data.get("variables", {})
    resolved_vars: Dict[str, Tuple[str, AttributePath]] = {}
    manifest_parser: "YAMLManifestParser" = cast("YAMLManifestParser", parser_context)
    substitution = manifest_parser.substitution
    for key, value_raw in variables.items():
        key_path = variables_path[key]
        if not SUBST_VAR_RE.match("{{" + key + "}}"):
            raise ManifestParseException(
                f"The variable at {key_path.path_key_lc} has an invalid name and therefore cannot"
                " be used."
            )
        if substitution.variable_state(key) != VariableNameState.UNDEFINED:
            raise ManifestParseException(
                f'The variable "{key}" is already reserved/defined. Error triggered by'
                f" {key_path.path_key_lc}."
            )
        try:
            value = substitution.substitute(value_raw, key_path.path)
        except DebputySubstitutionError:
            if not resolved_vars:
                raise
            # See if flushing the variables work
            substitution = manifest_parser.add_extra_substitution_variables(
                **resolved_vars
            )
            resolved_vars = {}
            value = substitution.substitute(value_raw, key_path.path)
        resolved_vars[key] = (value, key_path)
        substitution = manifest_parser.add_extra_substitution_variables(**resolved_vars)


def _handle_installation_rules(
    _name: str,
    parsed_data: List[InstallRule],
    _attribute_path: AttributePath,
    _parser_context: ParserContextData,
) -> List[InstallRule]:
    return parsed_data


def _handle_remove_during_clean(
    _name: str,
    parsed_data: List[RemoveDuringCleanParsedFormat],
    _attribute_path: AttributePath,
    _parser_context: ParserContextData,
) -> List[FileSystemMatchRule]:
    return [x["pattern"] for x in parsed_data]


def _handle_opaque_dict(
    _name: str,
    parsed_data: DictFormat,
    _attribute_path: AttributePath,
    _parser_context: ParserContextData,
) -> Dict[str, Any]:
    return parsed_data["mapping"]
